init
This commit is contained in:
commit
7539db0b12
20
LICENSE
Normal file
20
LICENSE
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2021 gd <gechandev@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
30
README
Normal file
30
README
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
__
|
||||||
|
|__| _____ ____ ______
|
||||||
|
| |/ \ / ___\/ ___/
|
||||||
|
| | Y Y / /_/ \___ \
|
||||||
|
|__|__|_| \___ /____ >
|
||||||
|
\/_____/ \/
|
||||||
|
|
||||||
|
imgs is a minimalictic image sharing web app written with Bottle framework.
|
||||||
|
|
||||||
|
No database. No image compression. No time limits. No additional dependencies.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
* Upload images via Drag&Drop
|
||||||
|
* Easy copy share link
|
||||||
|
* MIME type detecting
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
See deployment options in Bottle documentation: https://bottlepy.org/docs/dev/deployment.html
|
||||||
|
|
||||||
|
For local installation run:
|
||||||
|
|
||||||
|
$ python3 -m venv env
|
||||||
|
$ source env/bin/activate
|
||||||
|
$ pip install bottle
|
||||||
|
$ git clone https://github.com/gechandesu/imgs.git
|
||||||
|
$ python3 imgs/imgs.py
|
||||||
|
|
||||||
|
Edit the imgs.ini.
|
6
imgs.ini
Normal file
6
imgs.ini
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[imgs]
|
||||||
|
debug = False
|
||||||
|
base_url = http://localhost:5000/
|
||||||
|
image_name_lenght = 3
|
||||||
|
uploads_dir = uploads
|
||||||
|
allowed_mime_types = ['image/bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/tiff', 'image/webp']
|
85
imgs.py
Normal file
85
imgs.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from bottle import default_app as app
|
||||||
|
from bottle import run, get, post, request, response, error, template, static_file
|
||||||
|
|
||||||
|
|
||||||
|
config = app().config.load_config('./imgs.ini')
|
||||||
|
|
||||||
|
def generate_image_name(image: str) -> str:
|
||||||
|
name = ''
|
||||||
|
chars = string.ascii_letters + string.digits + '-_'
|
||||||
|
while len(name) <= int(config['imgs.image_name_lenght']) - 1:
|
||||||
|
name = name + random.choice(chars)
|
||||||
|
return name + os.path.splitext(image)[1].lower()
|
||||||
|
|
||||||
|
def get_base_url():
|
||||||
|
try:
|
||||||
|
base_url = config['imgs.base_url']
|
||||||
|
except KeyError:
|
||||||
|
base_url = request.url
|
||||||
|
return base_url
|
||||||
|
|
||||||
|
def get_image_url(image_name: str) -> str:
|
||||||
|
image_url = get_base_url() + '/' + image_name
|
||||||
|
return image_url.replace('//', '/').replace(':/', '://')
|
||||||
|
|
||||||
|
def upload_file(file):
|
||||||
|
image_name = generate_image_name(file.filename)
|
||||||
|
file.save(os.path.join(config['imgs.uploads_dir'], image_name))
|
||||||
|
return image_name
|
||||||
|
|
||||||
|
@error(404)
|
||||||
|
def error404(error):
|
||||||
|
return template('index.tpl',
|
||||||
|
uploaded = False, not_found = True, bad_mime_type = False,
|
||||||
|
base_url = get_base_url())
|
||||||
|
|
||||||
|
@get('/')
|
||||||
|
def index():
|
||||||
|
return template('index.tpl',
|
||||||
|
uploaded = False, not_found = False, bad_mime_type = False,
|
||||||
|
base_url = get_base_url())
|
||||||
|
|
||||||
|
@post('/')
|
||||||
|
def upload_image():
|
||||||
|
# Handle request from CLI
|
||||||
|
if request.files.get('image'):
|
||||||
|
file = request.files.get('image')
|
||||||
|
if file.content_type in config['imgs.allowed_mime_types']:
|
||||||
|
image_name = upload_file(file)
|
||||||
|
return get_image_url(image_name) + '\n'
|
||||||
|
else:
|
||||||
|
# Prevent recource leek. Force close buffered file
|
||||||
|
request.body.close()
|
||||||
|
response.status = 415
|
||||||
|
return 'Error: bad file MIME type\n'
|
||||||
|
# Handle request from web-browser
|
||||||
|
elif request.files.get('image_web'):
|
||||||
|
file = request.files.get('image_web')
|
||||||
|
if file.content_type in config['imgs.allowed_mime_types']:
|
||||||
|
image_name = upload_file(file)
|
||||||
|
return template('index.tpl',
|
||||||
|
uploaded = True, not_found = False, bad_mime_type = False,
|
||||||
|
base_url = get_base_url(), image_url = get_image_url(image_name))
|
||||||
|
else:
|
||||||
|
# Prevent recource leek. Force close buffered file
|
||||||
|
request.body.close()
|
||||||
|
response.status = 415
|
||||||
|
return template('index.tpl',
|
||||||
|
uploaded = False, not_found = False, bad_mime_type = True,
|
||||||
|
allowed_mime_types = config['imgs.allowed_mime_types'],
|
||||||
|
base_url = get_base_url(), image_url = 'None')
|
||||||
|
|
||||||
|
@get('/<image_name>')
|
||||||
|
def send_image(image_name):
|
||||||
|
return static_file(image_name, root = config['imgs.uploads_dir'])
|
||||||
|
|
||||||
|
@get('/style.css')
|
||||||
|
def send_style():
|
||||||
|
return static_file('style.css', root = './')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run(debug = config['imgs.debug'])
|
84
index.tpl
Normal file
84
index.tpl
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>imgs</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
|
||||||
|
% if not_found:
|
||||||
|
<div class="not-found">
|
||||||
|
<p><h1>404 Not found</h1></p>
|
||||||
|
<h1>(╯°□°)╯︵ ┻━┻</h1>
|
||||||
|
</div>
|
||||||
|
% end
|
||||||
|
|
||||||
|
% if bad_mime_type:
|
||||||
|
<div class="bad-mime-type">
|
||||||
|
<p><h1>415 Bad file MIME type</h1></p>
|
||||||
|
<h1>(´•ω•̥`)</h1>
|
||||||
|
</div>
|
||||||
|
% end
|
||||||
|
|
||||||
|
<div id="drop-area" class="drop-area" ondragover="dragOverHover();" ondragleave="dragLeave()">
|
||||||
|
<form action="/" method="POST" enctype="multipart/form-data">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"· fill="none" stroke="#000000" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 8l-5-5-5 5M12 4.2v10.3"/></svg><br>
|
||||||
|
<label for="image_web" class="file-input-label"><b> Choose images</b> or drag and drop it here</label>
|
||||||
|
<input class="file-input" type="file" multiple onchange="this.form.submit();" name='image_web' id='image_web'>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let dropArea = document.getElementById('drop-area');
|
||||||
|
function dragOverHover() {
|
||||||
|
dropArea.className = "drop-area dragover";
|
||||||
|
}
|
||||||
|
function dragLeave() {
|
||||||
|
dropArea.className = "drop-area";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p><b>or</b> upload images via cURL:</p>
|
||||||
|
|
||||||
|
<div class="curl">
|
||||||
|
<pre>curl -F 'image=@/path/to/image.jpg' {{ base_url }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
% if uploaded:
|
||||||
|
<div class="copy-to-clipboard">
|
||||||
|
<input type="text" value="{{ image_url }}" id="text-input">
|
||||||
|
<button onclick="CopyToClipboard()" onmouseout="mouseOut()">
|
||||||
|
<span id="copy-button">Copy URL</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let copyButton = document.getElementById("copy-button"),
|
||||||
|
copyText = document.getElementById("text-input");
|
||||||
|
|
||||||
|
function CopyToClipboard() {
|
||||||
|
copyText.select();
|
||||||
|
copyText.setSelectionRange(0, 99999); /*For mobile devices*/
|
||||||
|
document.execCommand("copy");
|
||||||
|
copyButton.innerHTML = "Copied!";
|
||||||
|
}
|
||||||
|
function mouseOut() {
|
||||||
|
copyButton.innerHTML = "Copy URL";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img src="{{ image_url }}" alt="{{ image_url }}" width="640">
|
||||||
|
|
||||||
|
<p>- - -</p>
|
||||||
|
% end
|
||||||
|
|
||||||
|
<div class="logo">
|
||||||
|
<pre> __<br>|__| _____ ____ ______<br>| |/ \ / ___\/ ___/<br>| | Y Y / /_/ \___ \<br>|__|__|_| \___ /____ ><br> \/_____/ \/</pre>
|
||||||
|
</div>
|
||||||
|
<p><a href="https://gitea.gch.icu/gd/imgs" target="_blank">v1.0</a></p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
bottle==0.12.19
|
85
style.css
Normal file
85
style.css
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
body {
|
||||||
|
background-color: #fff;
|
||||||
|
font-family: 'Ubuntu Mono', monospace;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main { margin: 4rem 2rem; }
|
||||||
|
.not-found, .bad-mime-type { margin-bottom: 2rem; }
|
||||||
|
|
||||||
|
a, a:visited { color: #000; }
|
||||||
|
|
||||||
|
img { width: 100%; }
|
||||||
|
|
||||||
|
/* Drag and Drop */
|
||||||
|
.drop-area {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 8rem;
|
||||||
|
padding: 25px;
|
||||||
|
border: 3px dashed #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-area.dragover { border-color: #000; }
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy to clipboard */
|
||||||
|
.copy-to-clipboard {
|
||||||
|
display: flex;
|
||||||
|
margin: 2rem 0;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-to-clipboard input[type=text] {
|
||||||
|
flex: 50%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-to-clipboard button {
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin: 0;
|
||||||
|
min-width: 120px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cURL command */
|
||||||
|
.curl pre {
|
||||||
|
text-align: left;
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo pre {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: left;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user