Compare commits

...
10 Commits
Author SHA1 Message Date
tanner 06b4cf08b9 Alert when new photos are uploaded 2026-01-24 18:35:32 +00:00
tanner 2fab3fa0d1 Update all dependencies 2024-06-30 19:36:36 +00:00
tanner 47933b7fea Update flask, pillow 2024-06-30 19:32:57 +00:00
tanner 964a586f10 Freeze requirements 2024-06-30 19:26:33 +00:00
tanner bcaa17765a Add dark mode, viewport meta 2024-02-26 01:29:40 +00:00
tanner c539325177 Thank geeknik in README 2024-02-24 18:04:39 +00:00
tanner 79e4aba172 Misc fixes 2024-02-24 18:03:37 +00:00
tanner 10d603f1c6 Accept application/octet-stream for CLI uploads 2024-02-24 17:57:01 +00:00
tanner 33d50d1ecb Convert other constants to variables 2024-02-24 17:41:50 +00:00
tanner d0f23aba5e Define app, put missing variables back 2024-02-24 17:36:48 +00:00
3 changed files with 74 additions and 47 deletions
+1 -1
View File
@@ -122,4 +122,4 @@ This project was inspired by how much imgur sucks!
Thanks to all the devs behind Flask, Pillow and Python. Thanks to all the devs behind Flask, Pillow and Python.
Thanks Andrew and Jason for the Javascript. Thanks Andrew and Jason for the Javascript, and geeknik for improvements.
+15 -9
View File
@@ -1,9 +1,15 @@
click==8.1.3 blinker==1.8.2
Flask==2.1.2 certifi==2026.1.4
importlib-metadata==4.11.4 charset-normalizer==3.4.4
itsdangerous==2.1.2 click==8.1.7
Jinja2==3.1.2 flask==3.0.3
MarkupSafe==2.1.1 idna==3.11
Pillow==9.1.1 importlib-metadata==8.0.0
Werkzeug==2.1.2 itsdangerous==2.2.0
zipp==3.8.0 jinja2==3.1.4
MarkupSafe==2.1.5
pillow==10.3.0
requests==2.32.5
urllib3==2.6.3
werkzeug==3.0.3
zipp==3.19.2
+58 -37
View File
@@ -3,58 +3,73 @@
import random import random
import string import string
import requests
from flask import abort, Flask, request, redirect from flask import abort, Flask, request, redirect
from pathlib import Path from pathlib import Path
from PIL import Image from PIL import Image
PICS = Path('data') app = Flask(__name__)
DATA_FOLDER = Path('data')
MAX_SIZE = 1920 MAX_SIZE = 1920
PORT = 5003 PORT = 5003
URL = 'https://pic.t0.vc' URL = 'https://pic.t0.vc'
POST = 'pic' POST_PARAM = 'pic'
MAX_ALLOWED_SIZE = 16 * 1024 * 1024 # 16 MB MAX_FILE_SIZE = 16 * 1024 * 1024 # 16 MB
def controller_message(message):
print('Sending controller message: ' + message)
payload = dict(pic=message)
r = requests.post('https://tbot.tannercollin.com/message', data=payload, timeout=5)
if r.status_code == 200:
return True
else:
print('Unable to communicate with controller!')
return False
def help(): def help():
form = ( form = (
'<form action="{0}" method="POST" accept-charset="UTF-8" enctype="multipart/form-data">' f'<form action="{URL}" method="POST" accept-charset="UTF-8" enctype="multipart/form-data">'
'<input name="web" type="hidden" value="true">' '<input name="web" type="hidden" value="true">'
'<input name="pic" type="file" accept="image/*" />' f'<input name="{POST_PARAM}" type="file" accept="image/*" />'
'<br><br><button type="submit">Submit</button></form>'.format(URL, POST) '<br><br><button type="submit">Submit</button></form>'
) )
return f""" return f"""
<meta name="color-scheme" content="light dark" />
<meta name="viewport" content="width=1%" />
<pre> <pre>
pic.t0.vc pic.t0.vc
NAME NAME
t0pic: command line image host. t0pic: command line image host.
USAGE USAGE
&lt;image output&gt; | curl -F '{0}=@/dev/stdin' {1} &lt;image output&gt; | curl -F '{POST_PARAM}=@/dev/stdin' {URL}
or upload from the web: or upload from the web:
{2} {form}
or paste the image into this page. or paste the image into this page.
DESCRIPTION DESCRIPTION
I got sick of imgur not working on mobile, so I built this I got sick of imgur not working on mobile, so I built this
Images are resized to a max dimension of {3} px Images are resized to a max dimension of {MAX_SIZE} px
Don't use this for anything serious Don't use this for anything serious
EXAMPLES EXAMPLES
~$ cat kitten.jpg | curl -F '{0}=@/dev/stdin' {1} ~$ cat kitten.jpg | curl -F '{POST_PARAM}=@/dev/stdin' {URL}
{1}/YXKV.jpg {URL}/YXKV.jpg
~$ firefox {1}/YXKV.jpg ~$ firefox {URL}/YXKV.jpg
Add this to your .bashrc: Add this to your .bashrc:
alias {0}="curl -F '{0}=@/dev/stdin' {1}" alias {POST_PARAM}="curl -F '{POST_PARAM}=@/dev/stdin' {URL}"
Now you can pipe directly into {0}! Now you can pipe directly into {POST_PARAM}!
SOURCE CODE SOURCE CODE
https://txt.t0.vc/CQQE https://txt.t0.vc/CQQE
nginx config: https://txt.t0.vc/ZKEH nginx config: https://txt.t0.vc/ZKEH
https://github.com/tannercollin/t0pic
SEE ALSO SEE ALSO
https://txt.t0.vc https://txt.t0.vc
@@ -62,23 +77,23 @@ SEE ALSO
</pre>""" </pre>"""
def paste(): def paste():
return """ return f"""
window.addEventListener('paste', e => { window.addEventListener('paste', e => {{
const file = e.clipboardData.items[0].getAsFile(); const file = e.clipboardData.items[0].getAsFile();
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
const image = new Image(); const image = new Image();
image.src = url; image.src = url;
const form = new FormData(); const form = new FormData();
form.append('pic', file); form.append('{POST_PARAM}', file);
fetch('/', { fetch('/', {{
method: 'POST', method: 'POST',
body: form body: form
}) }})
.then(r => r.text()) .then(r => r.text())
.then(u => window.location = u) .then(u => window.location = u)
.catch(e => alert(e.message)); .catch(e => alert(e.message));
}); }});
""" """
def new_id(): def new_id():
@@ -92,24 +107,25 @@ def index():
def new(): def new():
try: try:
if 'pic' not in request.files or request.files['pic'].filename == '': if 'pic' not in request.files or request.files['pic'].filename == '':
print("No file part") print('No file part')
abort(400) # No file was uploaded abort(400)
pic = request.files['pic'] pic = request.files['pic']
if pic.mimetype not in ['image/png', 'image/jpeg', 'image/gif']:
print(f"Unsupported image format: {pic.mimetype}")
abort(400) # Consider using a more specific status code or returning an error message
if pic.seek(0, 2) > MAX_ALLOWED_SIZE: # Move to end to get file size if pic.mimetype not in ['image/png', 'image/jpeg', 'image/gif', 'application/octet-stream']:
print(f"File size exceeds maximum allowed limit: {pic.tell()} bytes") print(f'Unsupported image format: {pic.mimetype}')
abort(413) # Payload Too Large abort(400)
if pic.seek(0, 2) > MAX_FILE_SIZE: # Move to end to get file size
print(f'File size exceeds maximum allowed limit: {pic.tell()} bytes')
abort(413)
pic.seek(0) # Reset file pointer pic.seek(0) # Reset file pointer
nid = new_id() nid = new_id()
while nid in [p.stem for p in PICS.iterdir()]: while nid in [p.stem for p in DATA_FOLDER.iterdir()]:
nid = new_id() nid = new_id()
pic = Image.open(pic.stream) # Use .stream to read directly from Flask file storage pic = Image.open(pic.stream)
if pic.format == 'PNG': if pic.format == 'PNG':
ext = '.png' ext = '.png'
@@ -118,25 +134,30 @@ def new():
elif pic.format == 'GIF': elif pic.format == 'GIF':
ext = '.gif' ext = '.gif'
else: else:
print(f"Unsupported image format: {pic.format}") print(f'Unsupported image format: {pic.format}')
abort(400) abort(400)
filename = nid + ext filename = nid + ext
pic.thumbnail([MAX_SIZE, MAX_SIZE], Image.Resampling.LANCZOS) pic.thumbnail([MAX_SIZE, MAX_SIZE], Image.Resampling.LANCZOS)
pic.save(PICS / filename) # Use pathlib's / operator for path concatenation pic.save(DATA_FOLDER / filename)
print(f'Adding pic {nid}{ext}') print(f'Adding pic {nid}{ext}')
url = f'{URL}/{nid}{ext}' url = f'{URL}/{nid}{ext}'
if pic.size != (300, 150):
msg = 'New t0pic:\n\n' + url
controller_message(msg)
if 'web' in request.form: if 'web' in request.form:
return redirect(url) return redirect(url)
else: else:
return f'{url}\n' return f'{url}\n'
except Exception as e: except Exception as e:
print(f"Error processing image upload: {e}") print(f'Error processing image upload: {e}')
abort(400) abort(400)
if __name__ == '__main__': if __name__ == '__main__':
if not PICS.exists(): if not DATA_FOLDER.exists():
PICS.mkdir(parents=True) DATA_FOLDER.mkdir(parents=True)
app.run(port=PORT, debug=False) # Turn off debug for production app.run(port=PORT, debug=False)