705 lines
23 KiB
Python
705 lines
23 KiB
Python
import flask
|
|
import shutil
|
|
import json
|
|
import os
|
|
import datetime
|
|
import random
|
|
import string
|
|
import uuid
|
|
from werkzeug.utils import secure_filename
|
|
from markupsafe import escape
|
|
import xml.etree.ElementTree as ET
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
from flask_wtf.csrf import CSRFProtect, generate_csrf
|
|
from flask_limiter import Limiter
|
|
from flask_limiter.util import get_remote_address
|
|
import magic
|
|
|
|
app = flask.Flask(__name__)
|
|
|
|
app.config['DATA_FILE'] = 'data.json'
|
|
app.config['BACKUP_DIR'] = 'backups'
|
|
app.config['STATIC_FOLDER'] = 'static'
|
|
app.config['UPLOAD_FOLDER'] = 'static/uploads'
|
|
app.config['CODE_FILE'] = 'code.txt'
|
|
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}
|
|
app.config['SITE_URL'] = 'https://posts.halhadus.rocks'
|
|
app.config['MAX_CONTENT_LENGTH'] = 32 * 1024 * 1024
|
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'set-your-stupid-secret-key')
|
|
app.config['WTF_CSRF_ENABLED'] = True
|
|
|
|
os.makedirs(app.config['STATIC_FOLDER'], exist_ok=True)
|
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
|
os.makedirs(app.config['BACKUP_DIR'], exist_ok=True)
|
|
|
|
csrf = CSRFProtect(app)
|
|
limiter = Limiter(app=app, key_func=get_remote_address)
|
|
|
|
def load_data():
|
|
try:
|
|
with open(app.config['DATA_FILE'], 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
sorted_items = sorted(
|
|
data.items(),
|
|
key=lambda x: x[1].get('date', 0),
|
|
reverse=True
|
|
)
|
|
return sorted_items
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return []
|
|
|
|
def format_timestamp(ts):
|
|
if not ts:
|
|
return ""
|
|
try:
|
|
dt = datetime.datetime.fromtimestamp(ts, datetime.timezone.utc)
|
|
return dt.strftime("%Y-%m-%d %H:%M UTC")
|
|
except:
|
|
return ""
|
|
|
|
def format_timestamp_rss(ts):
|
|
if not ts:
|
|
return ""
|
|
try:
|
|
datetime.datetime.utcfromtimestamp(ts)
|
|
return dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
|
except:
|
|
return ""
|
|
|
|
def get_upload_code():
|
|
code_file = app.config['CODE_FILE']
|
|
if os.path.exists(code_file):
|
|
with open(code_file, 'r') as f:
|
|
return f.read().strip()
|
|
else:
|
|
code = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
|
hashed_code = generate_password_hash(code)
|
|
with open(code_file, 'w') as f:
|
|
f.write(hashed_code)
|
|
print(f"Generated upload code (SAVE THIS, WON'T BE SHOWN AGAIN): {code}")
|
|
return hashed_code
|
|
|
|
def verify_upload_code(entered_code):
|
|
stored_hash = get_upload_code()
|
|
return check_password_hash(stored_hash, entered_code)
|
|
|
|
def create_backup():
|
|
source = app.config['DATA_FILE']
|
|
if not os.path.exists(source):
|
|
return None
|
|
|
|
backup_dir = app.config['BACKUP_DIR']
|
|
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
backup_file = os.path.join(backup_dir, f"data_backup_{timestamp}.json")
|
|
shutil.copy2(source, backup_file)
|
|
return backup_file
|
|
|
|
def cleanup_old_backups(max_backups=5):
|
|
backups = sorted(os.listdir(app.config['BACKUP_DIR']))
|
|
if len(backups) > max_backups:
|
|
for old_backup in backups[:-max_backups]:
|
|
os.remove(os.path.join(app.config['BACKUP_DIR'], old_backup))
|
|
|
|
def allowed_file(filename):
|
|
return '.' in filename and \
|
|
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
|
|
|
def validate_file(file_stream):
|
|
try:
|
|
header = file_stream.read(2048)
|
|
file_stream.seek(0)
|
|
mime_type = magic.from_buffer(header, mime=True)
|
|
return mime_type in ['image/png', 'image/jpeg', 'image/gif']
|
|
except Exception as e:
|
|
print(f"File validation error: {str(e)}")
|
|
return False
|
|
|
|
def generate_sitemap():
|
|
base_url = app.config['SITE_URL']
|
|
posts = load_data()
|
|
|
|
urlset = ET.Element('urlset', xmlns="http://www.sitemaps.org/schemas/sitemap/0.9")
|
|
|
|
url = ET.SubElement(urlset, 'url')
|
|
ET.SubElement(url, 'loc').text = base_url
|
|
ET.SubElement(url, 'lastmod').text = datetime.datetime.utcnow().strftime("%Y-%m-%d")
|
|
ET.SubElement(url, 'changefreq').text = 'daily'
|
|
ET.SubElement(url, 'priority').text = '1.0'
|
|
|
|
for post_id, post in posts:
|
|
url = ET.SubElement(urlset, 'url')
|
|
ET.SubElement(url, 'loc').text = f"{base_url}/#{post_id}"
|
|
if 'date' in post:
|
|
dt = datetime.datetime.utcfromtimestamp(post['date'])
|
|
ET.SubElement(url, 'lastmod').text = dt.strftime("%Y-%m-%d")
|
|
ET.SubElement(url, 'changefreq').text = 'monthly'
|
|
ET.SubElement(url, 'priority').text = '0.8'
|
|
|
|
return '<?xml version="1.0" encoding="UTF-8"?>' + ET.tostring(urlset, encoding='unicode')
|
|
|
|
def generate_rss():
|
|
base_url = app.config['SITE_URL']
|
|
posts = load_data()
|
|
|
|
rss = ET.Element('rss', version="2.0")
|
|
channel = ET.SubElement(rss, 'channel')
|
|
|
|
ET.SubElement(channel, 'title').text = "Halhadus' Posts"
|
|
ET.SubElement(channel, 'link').text = base_url
|
|
ET.SubElement(channel, 'description').text = "Personal posts and updates from Halhadus"
|
|
ET.SubElement(channel, 'language').text = "en-us"
|
|
ET.SubElement(channel, 'lastBuildDate').text = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")
|
|
ET.SubElement(channel, 'generator').text = "Halhadus Posts RSS Generator"
|
|
|
|
for post_id, post in posts:
|
|
item = ET.SubElement(channel, 'item')
|
|
|
|
title = post['text'].strip().split('\n')[0][:50] + ('...' if len(post['text']) > 50 else '')
|
|
ET.SubElement(item, 'title').text = title
|
|
|
|
post_url = f"{base_url}/#{post_id}"
|
|
ET.SubElement(item, 'link').text = post_url
|
|
ET.SubElement(item, 'guid').text = post_url
|
|
|
|
description_content = f"{post['text']}"
|
|
if 'image' in post and post['image']:
|
|
img_url = flask.url_for('static', filename=post['image'], _external=True)
|
|
description_content += f'<br><img src="{img_url}" alt="Post image">'
|
|
|
|
description = ET.SubElement(item, 'description')
|
|
description.text = description_content
|
|
|
|
if 'date' in post and post['date']:
|
|
pub_date = format_timestamp_rss(post['date'])
|
|
ET.SubElement(item, 'pubDate').text = pub_date
|
|
|
|
return '<?xml version="1.0" encoding="UTF-8"?>' + ET.tostring(rss, encoding='unicode')
|
|
|
|
@app.route('/')
|
|
def index():
|
|
data = load_data()
|
|
return flask.render_template_string('''
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Halhadus' Posts</title>
|
|
<meta name="description" content="Personal posts and updates from Halhadus">
|
|
<meta name="author" content="Halhadus">
|
|
<meta property="og:title" content="Halhadus' Posts">
|
|
<meta property="og:description" content="Personal posts and updates from Halhadus">
|
|
<meta property="og:url" content="{{ site_url }}">
|
|
<meta property="og:image" content="https://halhadus.rocks/assets/favicon.png">
|
|
<meta property="og:type" content="website">
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="{{ url_for('rss_feed') }}">
|
|
<link rel="icon" type="image/png" href="https://halhadus.rocks/assets/favicon.png">
|
|
<style>
|
|
:root {
|
|
--bg-color: #1f1f1f;
|
|
--card-bg: #2a2a2a;
|
|
--border-color: #333;
|
|
--text-color: #ffffff;
|
|
--accent-color: #4361ee;
|
|
--font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
font-family: var(--font-family);
|
|
line-height: 1.6;
|
|
padding: 20px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
header {
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
padding: 30px 0 20px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2.2rem;
|
|
margin-bottom: 5px;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
.site-link {
|
|
color: var(--accent-color);
|
|
text-decoration: none;
|
|
font-size: 0.95rem;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.site-link:hover {
|
|
opacity: 0.8;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.rss-link {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
color: #ff6600;
|
|
text-decoration: none;
|
|
font-size: 0.9rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.rss-link:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.git-link {
|
|
color: #6cc644;
|
|
text-decoration: none;
|
|
font-size: 0.9rem;
|
|
margin-left: 15px;
|
|
}
|
|
|
|
.git-link:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.posts-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 25px;
|
|
}
|
|
|
|
.post-card {
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.post-image {
|
|
width: 100%;
|
|
max-height: 400px;
|
|
object-fit: contain;
|
|
display: block;
|
|
margin-top: 15px;
|
|
background: #1a1a1a;
|
|
padding: 10px;
|
|
}
|
|
|
|
.post-content {
|
|
padding: 20px;
|
|
}
|
|
|
|
.post-text {
|
|
font-size: 1.05rem;
|
|
margin-bottom: 15px;
|
|
white-space: pre-wrap;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.post-date {
|
|
color: #8a8a8a;
|
|
font-size: 0.85rem;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: #666;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 10px;
|
|
}
|
|
|
|
header {
|
|
padding: 20px 0 15px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.8rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>Halhadus' Posts</h1>
|
|
<div>
|
|
<a href="https://halhadus.rocks" class="site-link">Halhadusite</a>
|
|
<a href="https://git.halhadus.rocks/Halhadus/my-posts" class="git-link">
|
|
Source Code
|
|
</a>
|
|
</div>
|
|
<a href="{{ url_for('rss_feed') }}" class="rss-link">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M4 11a9 9 0 0 1 9 9"></path>
|
|
<path d="M4 4a16 16 0 0 1 16 16"></path>
|
|
<circle cx="5" cy="19" r="1"></circle>
|
|
</svg>
|
|
RSS Feed
|
|
</a>
|
|
</header>
|
|
|
|
<main class="posts-grid">
|
|
{% if data %}
|
|
{% for id, post in data %}
|
|
<div class="post-card" id="{{ id }}">
|
|
<div class="post-content">
|
|
<p class="post-text">{{ post.text | e }}</p>
|
|
{% if 'date' in post and post.date %}
|
|
<div class="post-date">
|
|
{{ format_timestamp(post.date) }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if 'image' in post and post.image %}
|
|
<img
|
|
src="{{ url_for('static', filename=post.image) }}"
|
|
alt="Post image"
|
|
class="post-image"
|
|
onerror="this.style.display='none'">
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>No posts available</p>
|
|
</div>
|
|
{% endif %}
|
|
</main>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
''',
|
|
data=data,
|
|
site_url=app.config['SITE_URL'],
|
|
format_timestamp=format_timestamp)
|
|
|
|
@app.route('/upload', methods=['GET', 'POST'])
|
|
@limiter.limit("10 per minute")
|
|
#@csrf.exempt
|
|
def upload():
|
|
if flask.request.method == 'POST':
|
|
entered_code = flask.request.form.get('code')
|
|
if not entered_code or not verify_upload_code(entered_code):
|
|
return flask.render_template_string('''
|
|
<script>
|
|
alert("Invalid code!");
|
|
window.history.back();
|
|
</script>
|
|
''')
|
|
|
|
text = flask.request.form.get('text')
|
|
if not text:
|
|
return flask.render_template_string('''
|
|
<script>
|
|
alert("Text content is required!");
|
|
window.history.back();
|
|
</script>
|
|
''')
|
|
|
|
image_filename = None
|
|
if 'image' in flask.request.files:
|
|
file = flask.request.files['image']
|
|
if file.filename != '' and file:
|
|
if not allowed_file(file.filename):
|
|
return flask.render_template_string('''
|
|
<script>
|
|
alert("Invalid file type!");
|
|
window.history.back();
|
|
</script>
|
|
''')
|
|
|
|
if not validate_file(file.stream):
|
|
return flask.render_template_string('''
|
|
<script>
|
|
alert("Invalid file content!");
|
|
window.history.back();
|
|
</script>
|
|
''')
|
|
|
|
filename = secure_filename(file.filename)
|
|
unique_id = uuid.uuid4().hex
|
|
unique_filename = f"{unique_id}_{filename}"
|
|
file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
|
|
|
|
file.save(file_path)
|
|
image_filename = os.path.join('uploads', unique_filename)
|
|
|
|
new_post = {
|
|
'text': text,
|
|
'date': int(datetime.datetime.now().timestamp())
|
|
}
|
|
if image_filename:
|
|
new_post['image'] = image_filename
|
|
|
|
backup_file = create_backup()
|
|
cleanup_old_backups()
|
|
|
|
try:
|
|
if os.path.exists(app.config['DATA_FILE']):
|
|
with open(app.config['DATA_FILE'], 'r+', encoding='utf-8') as f:
|
|
try:
|
|
data = json.load(f)
|
|
except json.JSONDecodeError:
|
|
data = {}
|
|
else:
|
|
data = {}
|
|
|
|
post_id = f"post_{int(datetime.datetime.now().timestamp() * 1000)}"
|
|
data[post_id] = new_post
|
|
|
|
temp_file = app.config['DATA_FILE'] + '.tmp'
|
|
with open(temp_file, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
|
|
os.replace(temp_file, app.config['DATA_FILE'])
|
|
|
|
except Exception as e:
|
|
print(f"Error saving post: {str(e)}")
|
|
|
|
is_corrupted = False
|
|
try:
|
|
with open(app.config['DATA_FILE'], 'r', encoding='utf-8') as f:
|
|
json.load(f)
|
|
except:
|
|
is_corrupted = True
|
|
|
|
if is_corrupted and backup_file and os.path.exists(backup_file):
|
|
try:
|
|
shutil.copy2(backup_file, app.config['DATA_FILE'])
|
|
print(f"Restored data from backup: {backup_file}")
|
|
return flask.render_template_string('''
|
|
<script>
|
|
alert("Data file corrupted! Restored from backup. Please try again.");
|
|
window.location.href = "/upload";
|
|
</script>
|
|
''')
|
|
except Exception as restore_error:
|
|
print(f"Error restoring backup: {str(restore_error)}")
|
|
|
|
return flask.render_template_string('''
|
|
<script>
|
|
alert("Error saving post!");
|
|
window.history.back();
|
|
</script>
|
|
''')
|
|
|
|
return flask.redirect('/')
|
|
|
|
csrf_token = generate_csrf()
|
|
return flask.render_template_string('''
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Add New Post</title>
|
|
<meta name="description" content="Add a new post to Halhadus' Posts">
|
|
<meta property="og:title" content="Add New Post">
|
|
<meta property="og:url" content="{{ site_url }}/upload">
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
<link rel="icon" type="image/png" href="https://halhadus.rocks/assets/favicon.png">
|
|
<style>
|
|
:root {
|
|
--bg-color: #1f1f1f;
|
|
--card-bg: #2a2a2a;
|
|
--border-color: #333;
|
|
--text-color: #ffffff;
|
|
--accent-color: #4361ee;
|
|
--font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
font-family: var(--font-family);
|
|
line-height: 1.6;
|
|
padding: 20px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.container {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
header {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
padding: 20px 0;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
textarea, input[type="password"], input[type="file"] {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
color: var(--text-color);
|
|
font-family: var(--font-family);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
textarea {
|
|
min-height: 150px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.submit-btn {
|
|
background: var(--accent-color);
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 24px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-family: var(--font-family);
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
width: 100%;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.submit-btn:hover {
|
|
background: #3a51d8;
|
|
}
|
|
|
|
.info-note {
|
|
color: #8a8a8a;
|
|
font-size: 0.85rem;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.back-link {
|
|
display: inline-block;
|
|
margin-top: 20px;
|
|
color: var(--accent-color);
|
|
text-decoration: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 10px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.8rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>Add New Post</h1>
|
|
</header>
|
|
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<div class="form-group">
|
|
<label for="text">Content:</label>
|
|
<textarea name="text" id="text" required placeholder="Write your post content here..."></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="image">Image (optional):</label>
|
|
<input type="file" name="image" id="image" accept="image/*">
|
|
<p class="info-note">Supported formats: PNG, JPG, JPEG, GIF</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="code">Upload Code:</label>
|
|
<input type="password" name="code" id="code" required placeholder="Enter your 32-character upload code">
|
|
</div>
|
|
|
|
<button type="submit" class="submit-btn">Publish Post</button>
|
|
</form>
|
|
|
|
<a href="/" class="back-link">← Back to posts</a>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
''', site_url=app.config['SITE_URL'], csrf_token=csrf_token)
|
|
|
|
@app.route('/sitemap.xml')
|
|
def sitemap():
|
|
sitemap_xml = generate_sitemap()
|
|
response = flask.make_response(sitemap_xml)
|
|
response.headers['Content-Type'] = 'application/xml'
|
|
return response
|
|
|
|
@app.route('/robots.txt')
|
|
def robots():
|
|
robots_txt = f"""User-agent: *
|
|
Allow: /
|
|
Disallow: /upload
|
|
Disallow: /static/uploads/
|
|
|
|
Sitemap: {app.config['SITE_URL']}/sitemap.xml
|
|
"""
|
|
response = flask.make_response(robots_txt)
|
|
response.headers['Content-Type'] = 'text/plain'
|
|
return response
|
|
|
|
@app.route('/rss.xml')
|
|
def rss_feed():
|
|
rss_xml = generate_rss()
|
|
response = flask.make_response(rss_xml)
|
|
response.headers['Content-Type'] = 'application/rss+xml'
|
|
return response
|
|
|
|
if __name__ == '__main__':
|
|
if not os.path.exists(app.config['DATA_FILE']):
|
|
with open(app.config['DATA_FILE'], 'w', encoding='utf-8') as f:
|
|
sample_data = {
|
|
"firstpost": {
|
|
"text": "Hello, world!",
|
|
"date": int(datetime.datetime.now().timestamp())
|
|
},
|
|
}
|
|
json.dump(sample_data, f, indent=2)
|
|
|
|
get_upload_code()
|
|
|
|
app.run(
|
|
port=8080,
|
|
)
|