Compare commits
22 Commits
8800b519c0
...
master
Author | SHA1 | Date | |
---|---|---|---|
71e1d87d32 | |||
921dd5d29d | |||
cde343d159 | |||
cd258011be | |||
aed21e14a3 | |||
85b2664483 | |||
dbdbf2a3d9 | |||
3a8bef4ddc | |||
8122de2c52 | |||
2eea050bf8 | |||
2f7fc76573 | |||
dd570f7f93 | |||
312efc9047 | |||
fc80d9e661 | |||
721fda8b22 | |||
331a377c6b | |||
12f4433e8e | |||
0898b1f593 | |||
4ff6362c64 | |||
8e4ab793f9 | |||
87f6c3a048 | |||
397aeed123 |
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM alpine:latest
|
||||||
|
RUN apk update && apk add python3 py3-pip
|
||||||
|
ADD . /opt/imgs
|
||||||
|
RUN mkdir -p /opt/imgs/uploads
|
||||||
|
WORKDIR /opt/imgs
|
||||||
|
RUN pip install --upgrade pip && pip install --requirement requirements.txt
|
||||||
|
RUN pip install gunicorn
|
||||||
|
EXPOSE 5000
|
||||||
|
CMD gunicorn imgs:app --bind :5000
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2021 gd <gechandev@gmail.com>
|
Copyright (c) 2022 ge <gechandev@gmail.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of this software and associated documentation files (the
|
a copy of this software and associated documentation files (the
|
||||||
|
66
README.md
66
README.md
@ -5,8 +5,74 @@ imgs is a minimalictic image sharing web app written with Bottle framework.
|
|||||||
No database. No image compression. No time limits. No additional dependencies.
|
No database. No image compression. No time limits. No additional dependencies.
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
|
||||||
* Upload images via Drag&Drop
|
* Upload images via Drag&Drop
|
||||||
* Easy copy share link
|
* Easy copy share link
|
||||||
* MIME type detecting
|
* MIME type detecting
|
||||||
|
|
||||||
See deployment options in Bottle documentation: https://bottlepy.org/docs/dev/deployment.html
|
See deployment options in Bottle documentation: https://bottlepy.org/docs/dev/deployment.html
|
||||||
|
|
||||||
|
# Run imgs in Docker
|
||||||
|
|
||||||
|
Clone repository and edit **imgs.ini**.
|
||||||
|
|
||||||
|
Build Docker image:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker build --tag imgs .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run container from image. Replace **/path/to/your/uploads/dir** with path to directory where you want to store images:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -d \
|
||||||
|
--name imgs \
|
||||||
|
--publish 127.0.0.1:5000:5000 \
|
||||||
|
--volume /path/to/your/uploads/dir:/opt/imgs/uploads \
|
||||||
|
imgs
|
||||||
|
```
|
||||||
|
|
||||||
|
imgs will launched on `127.0.0.1:5000`. Set up reverse proxy server. I recommed to use basic authentication to prevent abuses. Nginx virtual host example:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourdomain.tld;
|
||||||
|
root /path/to/imgs/root;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
auth_basic "Authentication required";
|
||||||
|
auth_basic_user_file /path/to/.htpasswd;
|
||||||
|
proxy_pass http://127.0.0.1:5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* ^/favicon.ico$ {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \..* {
|
||||||
|
auth_basic off;
|
||||||
|
proxy_pass http://127.0.0.1:5000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Additional
|
||||||
|
|
||||||
|
## imgs client with CLI
|
||||||
|
|
||||||
|
imgs has a simple CLI tool based on curl. Copy **imgs** script to your PATH.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo cp imgs /usr/bin/imgs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nautilus integration
|
||||||
|
|
||||||
|
Push files to your imgs instance via GNOME Files (former name: Nautilus). Depends on: curl, libnotify (notify-send utility).
|
||||||
|
|
||||||
|
Just place **Upload to imgs** script into **~/.local/share/nautilus/scripts/** directory.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
DIR=~/.local/share/nautilus/scripts/; mkdir -p $DIR && cp Upload\ to\ imgs $DIR
|
||||||
|
```
|
||||||
|
32
Upload to imgs
Executable file
32
Upload to imgs
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# This is an imgs https://git.nxhs.cloud/ge/imgs "integration" for Nautilus.
|
||||||
|
# Place this script into path: $HOME/.local/share/nautilus/scripts
|
||||||
|
# See more info at: <https://help.ubuntu.com/community/NautilusScriptsHowto>
|
||||||
|
|
||||||
|
IMGSLOG="${IMGSLOG:-$HOME/imgs_debug.log}"
|
||||||
|
[ -n "$IMGSREMOTE" ] && return 0 # exit from func if variable is set
|
||||||
|
|
||||||
|
if [ -f "$HOME"/.imgsremote ]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$HOME"/.imgsremote
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$IMGSREMOTE" ]; then
|
||||||
|
echo "$0: Error: IMGSREMOTE variable is not set." >&2; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$IMGSDEBUG" ] && date +"[%d %b %Y %H:%M:%S] Started" >> "$IMGSLOG"
|
||||||
|
|
||||||
|
echo "$NAUTILUS_SCRIPT_SELECTED_FILE_PATHS" | while read -r file; do
|
||||||
|
[ -z "$file" ] && break
|
||||||
|
if [ -n "$IMGSDEBUG" ]; then
|
||||||
|
image="$(curl -v -L -F "image=@$file" "$IMGSREMOTE" 2>&1 | tee -a "$IMGSLOG")"
|
||||||
|
image="$(echo "$image" | tail -n 1)"
|
||||||
|
else
|
||||||
|
image="$(curl -L -F "image=@$file" "$IMGSREMOTE")"
|
||||||
|
fi
|
||||||
|
[ -n "$IMGSDEBUG" ] && echo "$(date +"[%d %b %Y %H:%M:%S]") $file --> $image" >> "$IMGSLOG"
|
||||||
|
notify-send "File uploaded to imgs!" "$image"
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$IMGSDEBUG" ] && date +"[%d %b %Y %H:%M:%S] Finished" >> "$IMGSLOG"
|
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
74
imgs
Executable file
74
imgs
Executable file
@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# imgs CLI https://git.nxhs.cloud/ge/imgs
|
||||||
|
|
||||||
|
imgs_usage() {
|
||||||
|
cat <<- EOF
|
||||||
|
Upload images to remote imgs server.
|
||||||
|
|
||||||
|
Usage: imgs [-rvh] <file>...
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-r, --remote remote imgs instance URI e.g. https://user:password@example.org
|
||||||
|
-v, --version print version and exit.
|
||||||
|
-h, --help print this help message and exit.
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
IMGSREMOTE remote imgs instance URI.
|
||||||
|
IMGSDEBUG enables verbose mode and logging.
|
||||||
|
IMGSLOG path to logfile. Default: ~/imgs_debug.log
|
||||||
|
|
||||||
|
You can set variables in ~/.imgsremote file instead of ~/.bashrc
|
||||||
|
See <https://git.nxhs.cloud/ge/imgs> for more info.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$#" -eq 0 ] && { imgs_usage; exit 1; }
|
||||||
|
|
||||||
|
# Transform long options to short ones
|
||||||
|
for arg in "$@"; do
|
||||||
|
shift
|
||||||
|
case "$arg" in
|
||||||
|
--remote) set -- "$@" "-r";;
|
||||||
|
--help) set -- "$@" "-h";;
|
||||||
|
--version) set -- "$@" "-v";;
|
||||||
|
*) set -- "$@" "$arg";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
while getopts r:vh OPT; do
|
||||||
|
case "$OPT" in
|
||||||
|
r) IMGSREMOTE="$OPTARG";;
|
||||||
|
v) echo 'imgs CLI 1.1'; exit 0;;
|
||||||
|
h) imgs_usage; exit 0;;
|
||||||
|
*) echo "$0: Unknown option: $OPT" >&2; exit 1;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
shift $((OPTIND - 1)) # shift for parse positional args
|
||||||
|
|
||||||
|
# Check variables
|
||||||
|
IMGSLOG="${IMGSLOG:-$HOME/imgs_debug.log}"
|
||||||
|
[ -n "$IMGSREMOTE" ] && return 0 # exit from func if variable is set
|
||||||
|
|
||||||
|
if [ -f "$HOME"/.imgsremote ]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$HOME"/.imgsremote
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$IMGSREMOTE" ]; then
|
||||||
|
echo "$0: Error: IMGSREMOTE variable is not set." >&2; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$IMGSDEBUG" ] && date +"[%d %b %Y %H:%M:%S] Started" | tee -a "$IMGSLOG"
|
||||||
|
|
||||||
|
for file in "$@"; do
|
||||||
|
filepath="$(realpath "$file")"
|
||||||
|
if [ -n "$IMGSDEBUG" ]; then
|
||||||
|
echo "Uploading $filepath ..." | tee -a "$IMGSLOG"
|
||||||
|
curl -v -L -F "image=@/$filepath" "$IMGSREMOTE" 2>&1 | tee -a "$IMGSLOG"
|
||||||
|
else
|
||||||
|
curl -L -F "image=@/$filepath" "$IMGSREMOTE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$IMGSDEBUG" ] && date +"[%d %b %Y %H:%M:%S] Finished" | tee -a "$IMGSLOG"
|
2
imgs.ini
2
imgs.ini
@ -1,6 +1,6 @@
|
|||||||
[imgs]
|
[imgs]
|
||||||
debug = False
|
|
||||||
base_url = http://localhost:5000/
|
base_url = http://localhost:5000/
|
||||||
image_name_lenght = 3
|
image_name_lenght = 3
|
||||||
uploads_dir = uploads
|
uploads_dir = uploads
|
||||||
|
#allowed_mime_types = * # allow any MIME type
|
||||||
allowed_mime_types = ['image/bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/tiff', 'image/webp']
|
allowed_mime_types = ['image/bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/tiff', 'image/webp']
|
||||||
|
44
imgs.py
44
imgs.py
@ -1,3 +1,5 @@
|
|||||||
|
__version__ = '1.1'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
@ -48,30 +50,38 @@ def upload_image():
|
|||||||
# Handle request from CLI
|
# Handle request from CLI
|
||||||
if request.files.get('image'):
|
if request.files.get('image'):
|
||||||
file = request.files.get('image')
|
file = request.files.get('image')
|
||||||
if file.content_type in config['imgs.allowed_mime_types']:
|
rq = 'cli'
|
||||||
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
|
# Handle request from web-browser
|
||||||
elif request.files.get('image_web'):
|
elif request.files.get('image_web'):
|
||||||
file = request.files.get('image_web')
|
file = request.files.get('image_web')
|
||||||
|
rq = 'web'
|
||||||
|
|
||||||
|
if config['imgs.allowed_mime_types'] == '*':
|
||||||
|
# Skip MIME checking.
|
||||||
|
image_name = upload_file(file)
|
||||||
|
else:
|
||||||
if file.content_type in config['imgs.allowed_mime_types']:
|
if file.content_type in config['imgs.allowed_mime_types']:
|
||||||
|
# Upload file!
|
||||||
image_name = upload_file(file)
|
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:
|
else:
|
||||||
|
# Show MIME type error!
|
||||||
# Prevent recource leek. Force close buffered file
|
# Prevent recource leek. Force close buffered file
|
||||||
request.body.close()
|
request.body.close()
|
||||||
response.status = 415
|
response.status = 415
|
||||||
return template('index.tpl',
|
if rq == 'cli':
|
||||||
uploaded = False, not_found = False, bad_mime_type = True,
|
return 'Error: bad file MIME type\n'
|
||||||
allowed_mime_types = config['imgs.allowed_mime_types'],
|
else:
|
||||||
base_url = get_base_url(), image_url = 'None')
|
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')
|
||||||
|
# Return 200 OK
|
||||||
|
if rq == 'cli':
|
||||||
|
return get_image_url(image_name) + '\n'
|
||||||
|
else:
|
||||||
|
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))
|
||||||
|
|
||||||
@get('/<image_name>')
|
@get('/<image_name>')
|
||||||
def send_image(image_name):
|
def send_image(image_name):
|
||||||
@ -81,5 +91,7 @@ def send_image(image_name):
|
|||||||
def send_style():
|
def send_style():
|
||||||
return static_file('style.css', root = './')
|
return static_file('style.css', root = './')
|
||||||
|
|
||||||
|
app = app() # Create WSGI application
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
run(debug = config['imgs.debug'])
|
run()
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
<div class="logo">
|
<div class="logo">
|
||||||
<pre> __<br>|__| _____ ____ ______<br>| |/ \ / ___\/ ___/<br>| | Y Y / /_/ \___ \<br>|__|__|_| \___ /____ ><br> \/_____/ \/</pre>
|
<pre> __<br>|__| _____ ____ ______<br>| |/ \ / ___\/ ___/<br>| | Y Y / /_/ \___ \<br>|__|__|_| \___ /____ ><br> \/_____/ \/</pre>
|
||||||
</div>
|
</div>
|
||||||
<p><a href="https://gitea.gch.icu/gd/imgs" target="_blank">v1.0</a></p>
|
<p><a href="https://git.nxhs.cloud/ge/imgs" target="_blank">v1.1</a></p>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1 +1 @@
|
|||||||
bottle==0.12.19
|
bottle==0.12.20
|
||||||
|
131
style.css
131
style.css
@ -1,85 +1,100 @@
|
|||||||
|
:root {
|
||||||
|
--b: #000;
|
||||||
|
--w: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--b: #fff;
|
||||||
|
--w: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #fff;
|
color: var(--b);
|
||||||
font-family: 'Ubuntu Mono', monospace;
|
background-color: var(--w);
|
||||||
max-width: 720px;
|
font-family: 'Ubuntu Mono', monospace;
|
||||||
margin: 0 auto;
|
max-width: 720px;
|
||||||
text-align: center;
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
main { margin: 4rem 2rem; }
|
main { margin: 4rem 2rem; }
|
||||||
.not-found, .bad-mime-type { margin-bottom: 2rem; }
|
a, a:visited { color: var(--b); }
|
||||||
|
|
||||||
a, a:visited { color: #000; }
|
|
||||||
|
|
||||||
img { width: 100%; }
|
img { width: 100%; }
|
||||||
|
.not-found, .bad-mime-type { margin-bottom: 2rem; }
|
||||||
|
.logo pre {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
/* Drag and Drop */
|
/* Drag and Drop */
|
||||||
.drop-area {
|
.drop-area {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 8rem;
|
height: 8rem;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
border: 3px dashed #e1e1e1;
|
border: 3px dashed #e1e1e1;
|
||||||
}
|
}
|
||||||
|
.drop-area.dragover { border-color: var(--b); }
|
||||||
.drop-area.dragover { border-color: #000; }
|
|
||||||
|
|
||||||
.file-input {
|
.file-input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input-label {
|
.file-input-label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Copy to clipboard */
|
/* Copy to clipboard */
|
||||||
.copy-to-clipboard {
|
.copy-to-clipboard {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
border: 1px solid #000000;
|
border: 1px solid var(--b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-to-clipboard input[type=text] {
|
.copy-to-clipboard input[type=text] {
|
||||||
flex: 50%;
|
flex: 50%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
background-color: var(--w);
|
||||||
|
color: var(--b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-to-clipboard button {
|
.copy-to-clipboard button {
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #000000;
|
background-color: var(--b);
|
||||||
color: #ffffff;
|
color: var(--w);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* cURL command */
|
/* cURL command */
|
||||||
.curl pre {
|
.curl pre {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background: #000000;
|
background: var(--b);
|
||||||
color: #ffffff;
|
color: var(--w);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo pre {
|
/* SVG */
|
||||||
display: flex;
|
svg {
|
||||||
justify-content: center;
|
color: var(--b);
|
||||||
text-align: left;
|
}
|
||||||
|
svg path {
|
||||||
|
stroke: currentcolor;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user