Making a Personal Image Host
Due to very specific technical requirements, I share images by uploading the content to an image hosting service, then distribute the resulting URLs through SMS. It looks incredibly stupid, but this method satisfies everyone involved. It shares the pictures with a third-party presenting a privacy concern, but I couldn't be bothered to find a better solution at the time.
Recently, the image hosting service started exhibiting several issues, prompting me to spend an evening making a tiny image host for personal use. Remember my VPN escapade? I am reusing that server to host a Flask application. Here is the Python code for this app:
from flask import Flask, request, render_template, send_from_directory
import os.path
import secrets
app = Flask(__name__)
UPLOAD_FOLDER = '/home/ubuntu/img/storage/'
@app.route('/')
def index():
return render_template('index.html')
@app.route('/storage/<path:name>')
def access_storage(name):
return send_from_directory(UPLOAD_FOLDER, name)
@app.route('/upload', methods=['POST'])
def upload_file():
results = []
for file in request.files.getlist('images[]'):
if file and file.filename != '':
ext = os.path.splitext(file.filename)[1]
filename = secrets.token_urlsafe(16) + ext
file.save(os.path.join(UPLOAD_FOLDER, filename))
results.append(filename)
return {'names': results}
Extremely simple code. The templates/index.html
accesses the upload endpoint to store the images—it's definitely fragile, but it does somehow work most of the time. And can you blame me, I wrote it in a day.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Upload</title>
<style>
html, body {
width: 100%;
height: 100%;
}
body {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
button {
width: 60%;
height: 20%;
font-size: 300%;
}
input {
display: none;
}
</style>
</head>
<body>
<button id="button" onclick="document.getElementById('imageInput').click()">按我上传图片</button>
<input type="file" id="imageInput" accept="image/*" onchange="submit()" multiple>
<script>
const imageInput = document.getElementById('imageInput');
const button = document.getElementById('button');
async function submit() {
button.setAttribute('disabled', 'disabled');
let i = 0;
setInterval(() => {
button.innerText = [
'请稍等.', '请稍等..', '请稍等...',
'豆豆正在思考.', '豆豆正在思考..', '豆豆正在思考...',
][i % 6];
i += 1;
}, 500);
const formData = new FormData();
Array.from(imageInput.files).forEach(file => {
formData.append('images[]', file);
});
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const names = (await response.json())['names'];
const paths = names.map(x => 'https://i26.mooo.com/storage/' + x);
await navigator.clipboard.writeText(paths.join('\n'));
alert('好啦, 豆豆发送了');
location.reload();
}
</script>
</body>
</html>
The UI/UX of this frontend is horrible, but I am catering to an audience of one, and that individual is happy with the experience. Anyway, I requested a subdomain from FreeDNS and added A records to point to the IP address of my server. I opened the 80 and 443 ports then ran certbot on the server to generate a certificate. Now, it's a simple matter of deploying with gunicorn, like this:
#!/bin/bash
source .venv/bin/activate
gunicorn \
--certfile=/etc/letsencrypt/live/i26.mooo.com/fullchain.pem \
--keyfile=/etc/letsencrypt/live/i26.mooo.com/privkey.pem \
-b 0.0.0.0:443 app:app
I run this script in the background with nohup so the process persists even when I exit the SSH session. And that's it, easy peasy. I was surprised at how straightforward it was to obtain a Let's Encrypt certificate, I thought I had to create TXT records or something which would have been inconvenient because it takes an hour for the FreeDNS records to propagate.