diff options
author | syn <isaqtm@gmail.com> | 2020-03-16 22:17:48 +0300 |
---|---|---|
committer | syn <isaqtm@gmail.com> | 2020-03-16 22:17:48 +0300 |
commit | 977c7c38ff4196f43c4784308d81fd7e6a471511 (patch) | |
tree | 3e53843c181a60ec7d2b3b1d4dfdf4868859c3a7 /app | |
download | blure-977c7c38ff4196f43c4784308d81fd7e6a471511.tar.gz |
Init commit
Diffstat (limited to 'app')
-rw-r--r-- | app/__init__.py | 38 | ||||
-rw-r--r-- | app/imutil.py | 61 | ||||
-rw-r--r-- | app/in_log.py | 57 | ||||
-rw-r--r-- | app/request_routine.py | 26 | ||||
-rw-r--r-- | app/schema.py | 37 | ||||
-rw-r--r-- | app/static/css/blure.css | 127 | ||||
-rw-r--r-- | app/static/css/eternal.css | 81 | ||||
-rw-r--r-- | app/static/js/eternalload.js | 120 | ||||
-rw-r--r-- | app/static/js/load.js | 6 | ||||
-rw-r--r-- | app/static/js/push.js | 74 | ||||
-rw-r--r-- | app/static/sass/blure.sass | 119 | ||||
-rw-r--r-- | app/static/sass/eternal.sass | 70 | ||||
-rw-r--r-- | app/templates/base.html.j2 | 18 | ||||
-rw-r--r-- | app/templates/index.html.j2 | 21 | ||||
-rw-r--r-- | app/templates/profile.html.j2 | 6 | ||||
-rw-r--r-- | app/util.py | 66 | ||||
-rw-r--r-- | app/views.py | 83 |
17 files changed, 1010 insertions, 0 deletions
diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..3fb828f --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,38 @@ +from sanic import Sanic +import config +from asyncpg import create_pool +import jinja2_sanic +from jinja2 import FileSystemLoader +import logging +from .in_log import LOG_SETTINGS +from .util import URLCoder +from .schema import schema_up + +blure = Sanic(__name__, log_config=LOG_SETTINGS) +log = logging.getLogger('blure') + +jinja2_sanic.setup(blure, loader=FileSystemLoader('app/templates')) +blure.config.from_object(config) +blure.static('/static', './app/static') +blure.url = URLCoder(blure.config.APP_SECRET[::2]) # cut secret, because it is too long + + +@blure.listener('before_server_start') +async def setup_db(_app, loop): + _app.pool = await create_pool(_app.config.PG_URI, loop=loop) + async with _app.pool.acquire() as conn: + await create_tables(conn) + + +@blure.listener('after_server_stop') +async def close_db_conns(_app, loop): + await _app.pool.close() + + +from . import views # noqa # unused-import, E402 + + +async def create_tables(conn): + async with conn.transaction(): + for statement in schema_up: + await conn.execute(statement) diff --git a/app/imutil.py b/app/imutil.py new file mode 100644 index 0000000..ec16dd1 --- /dev/null +++ b/app/imutil.py @@ -0,0 +1,61 @@ +from PIL import Image +from app import blure +from sanic.response import raw +from pathlib import Path +from io import BytesIO + +_IMAGE_URL = blure.config.NGX_IMAGE_URL +_IMAGE_PATH = blure.config.NGX_IMAGE_PATH +_NOT_FOUND_IMAGE = blure.config.NOT_FOUND_IMAGE +_NOT_FOUND_IMAGE_CONTENT_TYPE = blure.config.NOT_FOUND_IMAGE_CONTENT_TYPE + + +class NGXImage: + def __init__(self, id: int): + self.filename = blure.url.to_url(id) + + @staticmethod + def _load_pic(filename): + p = Path(_IMAGE_PATH.format(filename)) + if not p.is_file(): + return NGXImage.not_found() + + return raw(b'', + content_type='image', + headers={'X-Accel-Redirect': _IMAGE_URL.format(filename)}, + status=200) + + def orig(self): + return self._load_pic(self.filename) + + def thumb(self): + return self._load_pic(self.filename + '_thumb') + + @staticmethod + def not_found(): + return raw(_NOT_FOUND_IMAGE, + content_type=_NOT_FOUND_IMAGE_CONTENT_TYPE, + status=404) + + def save(self, body: BytesIO): + image_path = Path(_IMAGE_PATH.format(self.filename)) + thumb_path = Path(_IMAGE_PATH.format(self.filename + '_thumb')) + + with image_path.open('wb') as f: + f.write(body.getvalue()) + + with thumb_path.open('wb') as f: + im = Image.open(body) + thumb_stream = BytesIO() + im.thumbnail(blure.config.CUT_SIZES[2]) + im.save(thumb_stream, format='JPEG') + f.write(thumb_stream.getvalue()) + + def delete_from_disk(self): + image_path = Path(_IMAGE_PATH.format(self.filename)) + thumb_path = Path(_IMAGE_PATH.format(self.filename + '_thumb')) + + if image_path.exists(): + image_path.unlink() + if thumb_path.exists(): + thumb_path.unlink() diff --git a/app/in_log.py b/app/in_log.py new file mode 100644 index 0000000..77c85d6 --- /dev/null +++ b/app/in_log.py @@ -0,0 +1,57 @@ +time_with_col = '\033[96m%(asctime)s\033[0m ' + +LOG_SETTINGS = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'default': { + 'class': 'logging.StreamHandler', + 'level': 'DEBUG', + 'formatter': 'default', + }, + 'access_log': { + 'class': 'logging.StreamHandler', + 'level': 'DEBUG', + 'formatter': 'access' + }, + 'blure': { + 'class': 'logging.StreamHandler', + 'level': 'DEBUG', + 'formatter': 'default', + 'filters': [] + }, + }, + 'filters': {}, + 'formatters': { + 'default': { + 'format': time_with_col + '%(levelname)-7s %(name)-12s | %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S %z', + }, + 'access': { + 'format': time_with_col + 'ACCESS [%(host)s] \033[94m%(status)d\033[0m <- %(request)s', + 'datefmt': '%Y-%m-%d %H:%M:%S %z', + }, + }, + 'loggers': { + 'sanic.access': { + 'level': 'DEBUG', + 'handlers': ['access_log'], + 'propagate': False + }, + 'sanic.error': { + 'level': 'DEBUG', + 'handlers': ['default'], + 'propagate': True + }, + 'sanic.root': { + 'level': 'DEBUG', + 'handlers': ['default'], + 'propagate': True + }, + 'blure': { + 'level': 'DEBUG', + 'handlers': ['blure'], + 'propagate': True + } + } +} diff --git a/app/request_routine.py b/app/request_routine.py new file mode 100644 index 0000000..821272f --- /dev/null +++ b/app/request_routine.py @@ -0,0 +1,26 @@ +from functools import wraps +from collections import namedtuple +from app import blure as app + +Context = namedtuple('Context', [ + 'r', # request + 'pg', # pg connection + 'app' # r.app +]) + + +def db_route(route, pool=True, **route_kwargs): + def proxy_wrapper(f): + @app.route(route, **route_kwargs) + @wraps(f) + async def wrapper(request, *args, **kwargs): + if not pool: + ctx = Context(r=request, pg=None, app=request.app) + response = await f(ctx, *args, **kwargs) + else: + async with app.pool.acquire() as conn: + ctx = Context(r=request, pg=conn, app=request.app) + response = await f(ctx, *args, **kwargs) + return response + return wrapper + return proxy_wrapper diff --git a/app/schema.py b/app/schema.py new file mode 100644 index 0000000..247638f --- /dev/null +++ b/app/schema.py @@ -0,0 +1,37 @@ +schema_up = [ + ''' + CREATE TABLE IF NOT EXISTS pics ( + id SERIAL PRIMARY KEY UNIQUE, + src_url VARCHAR(1024), + ext VARCHAR(10), + deleted BOOLEAN + ); + ''', + ''' + CREATE TABLE IF NOT EXISTS tags ( + id SERIAL NOT NULL PRIMARY KEY, + tag_name VARCHAR(128) + ); + ''', + ''' + CREATE TABLE IF NOT EXISTS tags_pics ( + pic_id INT NOT NULL, + tag_id INT NOT NULL, + PRIMARY KEY (pic_id, tag_id), + FOREIGN KEY (pic_id) REFERENCES pics(id) ON UPDATE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON UPDATE CASCADE + ); + ''' +] + +schema_down = [ + 'DROP TABLE tags_pics', + 'DROP TABLE tags', + 'DROP TABLE pics' +] + +schema_delete = [ + 'DELETE * FROM tags_pics' + 'DELETE * FROM tags' + 'DELETE * FROM pics' +] diff --git a/app/static/css/blure.css b/app/static/css/blure.css new file mode 100644 index 0000000..acc1aab --- /dev/null +++ b/app/static/css/blure.css @@ -0,0 +1,127 @@ +body { + background-color: #182028 !important; +} + +#root { + font-family: "Inconsolata"; + font-size: 18px; + margin: auto; + max-width: 800px; + padding: 0 1em; + text-align: justify; + color: #eee; +} +#root img { + width: 100%; + margin-bottom: 0.5rem; +} + +a { + color: #846; + text-decoration: none; +} + +a:hover { + box-shadow: inset 0 0px 0 white, inset 0 -1px 0 #456; +} + +.profile-description { + padding: 0.5rem; + white-space: pre; +} + +.btn { + background: #283038; + color: #f6c; + border: 0px; + border-radius: 2px; + padding: 0.2em 0.4em; + font-size: 1em; + font-family: "Inconsolata"; + cursor: pointer; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 800; +} + +.progress-bg { + background: none; + filter: blur(3px); + -moz-filter: blur(3px); + -webkit-filter: blur(3px); + position: absolute; + width: 100px; + height: 100px; +} + +.progress-svg { + animation: progress-rotate 2s linear infinite; + height: 100px; + width: 100px; + position: relative; +} + +.progress-path { + stroke-dasharray: 0, calc(20 * 7); + stroke-linecap: round; + stroke: #888a; + -moz-transition: stroke-dasharray 0.3s ease; + -webkit-transition: stroke-dasharray 0.3s ease; + transition: stroke-dasharray 0.3s ease; +} + +@keyframes progress-rotate { + 100% { + transform: rotate(360deg); + } +} +.progress-wrapper { + display: inline-block; +} + +#push-url { + position: relative; + display: inline; +} + +#push-url-input { + min-width: 40%; + color: white; + background-color: transparent; + border: 1px solid transparent; + border-bottom-color: rgba(252, 49, 113, 0.2); +} + +#push-url-input:focus { + outline: none; +} + +#push-url-input::placeholder { + color: rgba(255, 255, 255, 0.3); +} + +#push-url-span { + position: absolute; + bottom: 0; + left: 50%; + width: 100%; + height: 1px; + opacity: 0; + background-color: #fc2f70; + transform-origin: center; + transform: translate(-50%, 0) scaleX(0); + transition: all 0.3s ease; +} + +#push-url-input:focus ~ span { + transform: translate(-50%, 0) scaleX(1); + opacity: 1; +} + +.progress-status { + width: 100%; + text-align: center; + font-size: 0.9em; +} + +/*# sourceMappingURL=blure.css.map */ diff --git a/app/static/css/eternal.css b/app/static/css/eternal.css new file mode 100644 index 0000000..e2c5895 --- /dev/null +++ b/app/static/css/eternal.css @@ -0,0 +1,81 @@ +.eternal-progress-bg { + background: none; + filter: blur(3px); + -moz-filter: blur(3px); + -webkit-filter: blur(3px); + position: absolute; + width: 100px; + height: 100px; +} + +.eternal-progress-svg { + animation: progress-rotate 2s linear infinite; + height: 100px; + width: 100px; + position: relative; +} + +.eternal-progress-path { + stroke-dasharray: 0, calc(20 * 7); + stroke-linecap: round; + stroke: #888a; + -moz-transition: stroke-dasharray 0.3s ease; + -webkit-transition: stroke-dasharray 0.3s ease; + transition: stroke-dasharray 0.3s ease; +} + +@keyframes progress-rotate { + 100% { + transform: rotate(360deg); + } +} +.eternal-progress-wrapper { + display: inline-block; +} + +.eternal-progress-status { + width: 100%; + text-align: center; + font-size: 0.9em; +} + +.eternal-url { + position: relative; + display: inline; +} + +.eternal-url-input { + min-width: 40%; + color: white; + background-color: transparent; + border: 1px solid transparent; + border-bottom-color: rgba(252, 49, 113, 0.2); +} + +.eternal-url-input:focus { + outline: none; +} + +.eternal-url-input::placeholder { + color: rgba(255, 255, 255, 0.3); +} + +.eternal-url-input ~ span { + position: absolute; + bottom: 0; + left: 50%; + width: 100%; + height: 1px; + opacity: 0; + background-color: #fc2f70; + transform-origin: center; + transform: translate(-50%, 0) scaleX(0); + transition: all 0.3s ease; +} + +.eternal-url-input:focus ~ span { + transform: translate(-50%, 0) scaleX(1); + opacity: 1; +} + +/*# sourceMappingURL=eternal.css.map */ diff --git a/app/static/js/eternalload.js b/app/static/js/eternalload.js new file mode 100644 index 0000000..fe23c98 --- /dev/null +++ b/app/static/js/eternalload.js @@ -0,0 +1,120 @@ +const progress_width = 100; +const progress_radius = 20; +const progress_len = progress_radius * 2 * Math.PI; + +const PREVIEW_TEMPLATE = ` +<div class="eternal-progress-wrapper"> + <div class="eternal-progress-bg"></div> + <svg class="eternal-progress-svg"> + <circle class="eternal-progress-path" + cx="${progress_width / 2}" + cy="${progress_width / 2}" + r="${progress_radius}" + fill="none" + stroke-width="5" + stroke-miterlimit="10" /> + </svg> + <pre class="eternal-progress-status"></pre> +</div> +` + +const REMOTE_LOAD_TEMPLATE = ` +<form method='POST' autocomplete="off"> + <input class="eternal-url-input" type="text" placeholder="Remote image"> + <span class="eternal-url-span"></span> +</form> +`.trim(); + +function append_template_for_file (node, file, fileno) { + const imageurl = URL.createObjectURL(file); + node.insertAdjacentHTML('beforeend', PREVIEW_TEMPLATE); + let newnode = node.lastChild.previousSibling; + console.log(newnode); + let style = newnode.querySelector('.eternal-progress-bg').style; + style.background = 'url(' + imageurl + ')'; + style.backgroundSize = 'cover'; + style.backgroundPosition = 'center'; + style.backgroundRepeat = 'no-repeat'; + newnode.setAttribute('id', 'progress-files-' + fileno.toString()); + + return newnode; +} + +class EternalLoad { + constructor (node) { + this.root = document.createElement('div'); + this.root.setAttribute('id', 'eternal-root'); + this.local = {}; + this.remote = {}; + } + + attach (target) { + if (this.local.btn) + this.root.appendChild(this.local.btn); + target.appendChild(this.root); + + return this; + } + + use_local (url) { + this.local.btn = document.createElement('button'); + this.local.btn.setAttribute('class', 'btn'); + this.local.btn.textContent = 'local file'; + + this.local.total = 0; + this.local.processed = 0; + this.local.page_hard_lock = 1; + + this.local.btn.addEventListener('click', (e) => { + let input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.multiple = true; + input.addEventListener('change', (input_event) => { + Array.prototype.forEach.call(input_event.target.files, file => { + let data = new FormData(); + data.append('im', file); + + let fileno = this.local.total; + this.local.total += 1; + let wrapper = append_template_for_file (this.root, file, fileno); + let progress = wrapper.querySelector('.eternal-progress-path'); + let status = wrapper.querySelector('.eternal-progress-status'); + + let req = new XMLHttpRequest(); + req.addEventListener('load', (e) => { + let bg = wrapper.querySelector('.eternal-progress-bg'); + bg.style.filter = 'none'; + progress.style.stroke = '#0000'; + + this.local.processed += 1; + if (this.local.processed == this.local.total && !this.local.page_hard_lock) + location.reload(); + }); + req.upload.addEventListener('progress', (e) => { + let complete = 0; + if (e.lengthComputable) + complete = e.loaded / e.total; + status.innerHTML = `${(e.loaded / 1000000).toFixed(2)}/${(e.total / 1000000).toFixed(2)}M`; + progress.style.strokeDasharray = `${(complete * Math.PI * 2 * progress_radius)},${progress_len}`; + + }); + + req.open('POST', url); + req.send(data); + }); + this.local.page_hard_lock = 0; + }); + + input.click(); + }); + + return this; + } + + use_remote (url) { + return this; + } +} + +export default EternalLoad; diff --git a/app/static/js/load.js b/app/static/js/load.js new file mode 100644 index 0000000..574841b --- /dev/null +++ b/app/static/js/load.js @@ -0,0 +1,6 @@ +import EternalLoad from './eternalload.js' + +let load = new EternalLoad() + .use_local('/c/push') + .use_remote('/c/push_url') + .attach(document.querySelector('#template-goes-here')); diff --git a/app/static/js/push.js b/app/static/js/push.js new file mode 100644 index 0000000..e6916c8 --- /dev/null +++ b/app/static/js/push.js @@ -0,0 +1,74 @@ +const progress_radius = 20; +const progress_len = progress_radius * 2 * Math.PI; + +window.addEventListener('DOMContentLoaded', function() { + let pusher = document.getElementById('push-image-btn'); + + let total = 0; + let processed = 0; + let page_hard_lock = 1; + + pusher.addEventListener('click', function(e) { + let input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.multiple = true; + input.addEventListener('change', (input_event) => { + console.log(input_event.target.files); + Array.prototype.forEach.call(input_event.target.files, file => { + let data = new FormData(); + data.append('im', file); + + let fileno = total; + total += 1; + let wrapper = append_template_for_file (file, fileno); + let progress = wrapper.querySelector('.progress-path'); + let status = wrapper.querySelector('.progress-status'); + + let req = new XMLHttpRequest(); + req.addEventListener('load', (e) => { + let bg = wrapper.querySelector('.progress-bg'); + bg.style.filter = 'none'; + progress.style.stroke = '#0000'; + + processed += 1; + if (processed == total && !page_hard_lock) { + location.reload(); + } + }); + req.upload.addEventListener('progress', (e) => { + let complete = 0; + if (e.lengthComputable) { + complete = e.loaded / e.total; + } + status.innerHTML = (e.loaded / 1000000).toFixed(2) + '/' + (e.total / 1000000).toFixed(2) + 'M'; + progress.style.strokeDasharray = (complete * Math.PI * 2 * progress_radius).toString () + ',' + progress_len.toString(); + + }); + + req.open('POST', '/c/push'); + req.send(data); + }); + page_hard_lock = 0; + }); + + input.click(); + }); +}); + +function append_template_for_file (file, fileno) { + let imageurl = URL.createObjectURL(file); + let tmpl = document.querySelector('#progress-template'); + let root = document.querySelector('#template-goes-here'); + let newnode = document.importNode(tmpl.content, true); + let style = newnode.querySelector('.progress-bg').style; + style.background = 'url(' + imageurl + ')'; + style.backgroundSize = 'cover'; + style.backgroundPosition = 'center'; + style.backgroundRepeat = 'no-repeat'; + root.appendChild(newnode); + let appended = root.lastChild.previousSibling; + appended.setAttribute('id', 'progress-files-' + fileno.toString()); + + return appended; +} diff --git a/app/static/sass/blure.sass b/app/static/sass/blure.sass new file mode 100644 index 0000000..e472fb7 --- /dev/null +++ b/app/static/sass/blure.sass @@ -0,0 +1,119 @@ +$sidenav-width: 200px +$primary-fg: #fff +$primary-bg: #182028 +$pink: #f6c + +$preview-size: 100px +$progress-radius: 20 + +$monospace: 'Inconsolata' + +body + background-color: $primary-bg !important + +#root + font-family: $monospace + font-size: 18px + margin: auto + max-width: 800px + padding: 0 1em + text-align: justify + color: #eee + + img + width: 100% + margin-bottom: .5rem + +a + color: #846 + text-decoration: none + +a:hover + box-shadow: inset 0 0px 0 white, inset 0 -1px 0 #456 + //border-bottom: 1px solid black + +.profile-description + padding: .5rem + white-space: pre + +.btn + background: #283038 + color: $pink + border: 0px + border-radius: 2px + padding: .2em .4em + font-size: 1em + font-family: $monospace + cursor: pointer + text-transform: uppercase + letter-spacing: .05em + font-weight: 800 + +.progress-bg + background: none + filter: blur(3px) + -moz-filter: blur(3px) + -webkit-filter: blur(3px) + position: absolute + width: $preview-size + height: $preview-size + +.progress-svg + animation: progress-rotate 2s linear infinite + height: $preview-size + width: $preview-size + position: relative + + +.progress-path + stroke-dasharray: 0, calc(#{$progress-radius} * 7) + stroke-linecap: round + stroke: #888a + -moz-transition: stroke-dasharray 0.3s ease + -webkit-transition: stroke-dasharray 0.3s ease + transition: stroke-dasharray 0.3s ease + +@keyframes progress-rotate + 100% + transform: rotate(360deg) + +.progress-wrapper + display: inline-block + +#push-url + position: relative + display: inline + +#push-url-input + min-width: 40% + color: white + background-color: transparent + border: 1px solid transparent + border-bottom-color: hsla(341, 97%, 59%, 0.2) + +#push-url-input:focus + outline: none + +#push-url-input::placeholder + color: hsla(0, 0%, 100%, 0.3) + +#push-url-span + position: absolute + bottom: 0 + left: 50% + width: 100% + height: 1px + opacity: 0 + background-color: #fc2f70 + transform-origin: center + transform: translate(-50%, 0) scaleX(0) + transition: all 0.3s ease + +#push-url-input:focus ~ span + transform: translate(-50%, 0) scaleX(1) + opacity: 1 + +.progress-status + width: 100% + text-align: center + font-size: 0.9em
\ No newline at end of file diff --git a/app/static/sass/eternal.sass b/app/static/sass/eternal.sass new file mode 100644 index 0000000..e6e1b56 --- /dev/null +++ b/app/static/sass/eternal.sass @@ -0,0 +1,70 @@ +$preview-size: 100px +$progress-radius: 20 + +.eternal-progress-bg + background: none + filter: blur(3px) + -moz-filter: blur(3px) + -webkit-filter: blur(3px) + position: absolute + width: $preview-size + height: $preview-size + +.eternal-progress-svg + animation: progress-rotate 2s linear infinite + height: $preview-size + width: $preview-size + position: relative + +.eternal-progress-path + stroke-dasharray: 0, calc(#{$progress-radius} * 7) + stroke-linecap: round + stroke: #888a + -moz-transition: stroke-dasharray 0.3s ease + -webkit-transition: stroke-dasharray 0.3s ease + transition: stroke-dasharray 0.3s ease + +@keyframes progress-rotate + 100% + transform: rotate(360deg) + +.eternal-progress-wrapper + display: inline-block + +.eternal-progress-status + width: 100% + text-align: center + font-size: 0.9em + +.eternal-url + position: relative + display: inline + +.eternal-url-input + min-width: 40% + color: white + background-color: transparent + border: 1px solid transparent + border-bottom-color: hsla(341, 97%, 59%, 0.2) + +.eternal-url-input:focus + outline: none + +.eternal-url-input::placeholder + color: hsla(0, 0%, 100%, 0.3) + +.eternal-url-input ~ span + position: absolute + bottom: 0 + left: 50% + width: 100% + height: 1px + opacity: 0 + background-color: #fc2f70 + transform-origin: center + transform: translate(-50%, 0) scaleX(0) + transition: all 0.3s ease + +.eternal-url-input:focus ~ span + transform: translate(-50%, 0) scaleX(1) + opacity: 1 diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2 new file mode 100644 index 0000000..b2c0957 --- /dev/null +++ b/app/templates/base.html.j2 @@ -0,0 +1,18 @@ +<html> + <head> + <title> + Eternal Blume + </title> + <link href="/static/css/blure.css" rel="stylesheet"> + <link href="https://fonts.googleapis.com/icon?family=Material+Icons|Inconsolata" rel="stylesheet"> + {% block load_scripts %} + {% endblock %} + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + </head> + <body> + <div id="root"> + {% block content %} + {% endblock %} + </div> + </body> +</html> diff --git a/app/templates/index.html.j2 b/app/templates/index.html.j2 new file mode 100644 index 0000000..f7d9746 --- /dev/null +++ b/app/templates/index.html.j2 @@ -0,0 +1,21 @@ +{% extends "base.html.j2" %} + +{% block load_scripts %} +<script type="module" src="/static/js/eternalload.js"></script> +<script type="module" src="/static/js/load.js"></script> +<link href="/static/css/eternal.css" rel="stylesheet"> +{% endblock %} + +{% block content %} + +<div id="template-goes-here"> +</div> + +{% for url in picurls %} + <a href="/p/{{ url }}"> + <img src="/t/{{ url }}" alt="{{ url }}" /></a> + <form action="/c/delete/{{ url }}" method="POST"> + <button class="btn" type="submit">rm</button> + </form> +{% endfor %} +{% endblock %} diff --git a/app/templates/profile.html.j2 b/app/templates/profile.html.j2 new file mode 100644 index 0000000..d4da6c4 --- /dev/null +++ b/app/templates/profile.html.j2 @@ -0,0 +1,6 @@ +{% extends "base.html.j2" %} +{% block content %} +<img src="/i/{{ url }}" /> +<div class="profile-description">url: <a href="/i/{{ url }}">{{ '/i/' + url }}</a> +date: 1986-04-26T21:23+0000</div> +{% endblock %}
\ No newline at end of file diff --git a/app/util.py b/app/util.py new file mode 100644 index 0000000..69b3ea7 --- /dev/null +++ b/app/util.py @@ -0,0 +1,66 @@ +from base64 import b32encode, b32decode +import logging +from hashlib import blake2b # because it is fast as fuck + +log = logging.getLogger('blure') + + +class URLCoder: + ''' + URLCoder shuffles sequential ids into non-sequential strings, + that can be used as urls + ''' + _pad = 1 + _bs = 2 + _bound = 2 ** (8 * _bs * 2) + _endian = 'big' + + @classmethod + def _humanify(self, data: bytes): + b32 = b32encode(data).decode('ascii') + if self._pad > 0: + return b32[:-self._pad] + else: + return b32 + + @classmethod + def _dehumanify(self, data: str): + binary = b32decode(data + '=' * self._pad) + assert len(binary) == self._bs * 2 + return binary + + def __init__(self, secret: bytes): + assert len(secret) % (self._bs) == 0 + + self.keys = [ + secret[key_start:key_start + self._bs] + for key_start in range(0, len(secret), self._bs) + ] + self.reverse_keys = self.keys[::-1] + + def to_url(self, id: int) -> str: + assert 0 <= id and id <= self._bound + as_bytes = int.to_bytes(id, self._bs * 2, self._endian) + encoded = self._blake_enc(as_bytes, self.keys) + return self._humanify(encoded) + + def to_id(self, url: str) -> int: + binary = self._dehumanify(url.upper()) + as_bytes = self._blake_enc(binary, self.reverse_keys) + return int.from_bytes(as_bytes, self._endian) + + def _blake_enc(self, data: bytes, keys: bytes): + bs = self._bs + + def xor(bytes1, bytes2): + return bytes([b1 ^ b2 for b1, b2 in zip(bytes1, bytes2)]) + + def blake_round(data, key): + return blake2b(data, digest_size=bs, key=key).digest() + + left = data[:bs] + right = data[bs:] + for key in keys: + left, right = right, xor(left, blake_round(right, key)) + + return right + left diff --git a/app/views.py b/app/views.py new file mode 100644 index 0000000..8ffb946 --- /dev/null +++ b/app/views.py @@ -0,0 +1,83 @@ +from io import BytesIO +from .request_routine import db_route +from sanic.response import text, redirect, json +from jinja2_sanic import render_template +from app import log +from requests import get as fetch_url +from .imutil import NGXImage + + +@db_route('/') +async def index(ctx): + records = await ctx.pg.fetch('SELECT id FROM pics') + picurls = [ctx.app.url.to_url(rec['id']) for rec in records] + return render_template('index.html.j2', ctx.r, dict(picurls=picurls)) + + +@db_route('/i/<url>') +async def raw_image(ctx, url): + id = ctx.app.url.to_id(url) + if id is None: + return NGXImage.not_found() + + name = await ctx.pg.fetchval('SELECT 1 FROM pics WHERE id=$1', id) + if name is None: + log.error('Image not in db') + return NGXImage.not_found() + + return NGXImage(id).orig() + + +@db_route('/t/<url>') +async def thumb_image(ctx, url): + id = ctx.app.url.to_id(url) + if id is None: + return NGXImage.not_found() + + name = await ctx.pg.fetchval('SELECT 1 FROM pics WHERE id=$1', id) + if name is None: + log.error('Image not in db') + return NGXImage.not_found() + + return NGXImage(id).thumb() + + +@db_route('/p/<url>') +async def pic_profile(ctx, url): + return render_template('profile.html.j2', ctx.r, dict(url=url, tags=[])) + + +@db_route('/c/push', methods=['POST']) +async def pic_push(ctx): + try: + file = ctx.r.files['im'][0] + image_stream = BytesIO(file.body) + ext = file.name.split('.')[-1] + id = await ctx.pg.fetchval( + 'INSERT INTO pics(src_url, ext) VALUES ($1, $2) RETURNING id', '', ext + ) + + im = NGXImage(id) + im.save(image_stream) + + return text('new url is ' + ctx.app.url.to_url(id)) + except KeyError: + return text('you did not post anything') + + +@db_route('/c/push_url', methods=['POST']) +async def push_url(ctx): + return redirect(ctx.app.url_for('index')) + return json({'err': 'not_implemented'}) + response = fetch_url(ctx.r.json['url']) + if response is None: + return json({'err': 'fetch'}) + + +@db_route('/c/delete/<url>', methods=['POST']) +async def delete_pic(ctx, url): + id = ctx.app.url.to_id(url) + await ctx.pg.execute('DELETE FROM pics WHERE id=$1', id) + im = NGXImage(id) + im.delete_from_disk() + return redirect(ctx.app.url_for('index')) |