init
new file: .gitignore new file: app.py new file: requirements.txt new file: templates/base.html.j2 new file: templates/edit_media.html.j2 new file: templates/home.html.j2 new file: templates/import_user_record.html.j2 new file: templates/list_media.html.j2 new file: templates/login.html.j2 new file: templates/profile.html.j2 new file: templates/upload_media.html.j2 new file: templates/user_media.html.j2 new file: templates/verify.html.j2 new file: templates/view_media.html.j2 new file: templates/view_media_details.html.j2
This commit is contained in:
commit
d7416d2bae
15 changed files with 1413 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.db
|
||||
962
app.py
Normal file
962
app.py
Normal file
|
|
@ -0,0 +1,962 @@
|
|||
# app.py
|
||||
|
||||
import os
|
||||
import base64
|
||||
import datetime
|
||||
import random
|
||||
import string
|
||||
import bcrypt
|
||||
import re
|
||||
import uuid
|
||||
import hashlib
|
||||
import smtplib
|
||||
import mimetypes
|
||||
import json
|
||||
import logging # Import the logging module
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from pyramid.config import Configurator
|
||||
from pyramid.view import view_config
|
||||
from pyramid.response import Response
|
||||
from pyramid.httpexceptions import HTTPFound, HTTPForbidden
|
||||
from pyramid.session import SignedCookieSessionFactory
|
||||
from sqlalchemy import (
|
||||
create_engine,
|
||||
Column,
|
||||
String,
|
||||
DateTime,
|
||||
Boolean,
|
||||
Integer,
|
||||
Index,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from waitress import serve
|
||||
|
||||
# Import renderers for templates
|
||||
from pyramid.renderers import render_to_response
|
||||
|
||||
# Import for events
|
||||
from pyramid.events import subscriber
|
||||
from pyramid_jinja2 import IJinja2Environment
|
||||
|
||||
################################################################################
|
||||
# Set up logging
|
||||
################################################################################
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
################################################################################
|
||||
# Helper Functions
|
||||
################################################################################
|
||||
|
||||
def slugify(text):
|
||||
text = text.lower()
|
||||
text = re.sub(r"\s+", "-", text)
|
||||
text = re.sub(r"[^\w\-]", "", text)
|
||||
return text
|
||||
|
||||
def get_gravatar_url(email, size=100):
|
||||
email = email.strip().lower()
|
||||
hash_code = hashlib.md5(email.encode("utf-8")).hexdigest()
|
||||
return f"https://www.gravatar.com/avatar/{hash_code}?s={size}&d=identicon"
|
||||
|
||||
def send_email(to_email, subject, body):
|
||||
# For testing purposes, print the email content to the console
|
||||
log.debug("======= Email Sent =======")
|
||||
log.debug(f"To: {to_email}")
|
||||
log.debug(f"Subject: {subject}")
|
||||
log.debug(f"Body:\n{body}")
|
||||
log.debug("==========================")
|
||||
|
||||
msg = MIMEText(body)
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = "noreply@example.com"
|
||||
msg["To"] = to_email
|
||||
|
||||
try:
|
||||
s = smtplib.SMTP("localhost", 25)
|
||||
s.sendmail("noreply@example.com", [to_email], msg.as_string())
|
||||
s.quit()
|
||||
except Exception as e:
|
||||
log.error(f"Error sending email: {e}")
|
||||
|
||||
def admin_required(view_func):
|
||||
def wrapper(request):
|
||||
user = request.user
|
||||
if not user or not user.is_admin:
|
||||
return HTTPForbidden("You must be an admin to access this page.")
|
||||
return view_func(request)
|
||||
return wrapper
|
||||
|
||||
def get_current_user(request):
|
||||
"""Return the current user (authenticated or guest) from session."""
|
||||
user_id = request.session.get("user_id")
|
||||
s = request.dbsession
|
||||
|
||||
if user_id:
|
||||
# Try to get the user from the database
|
||||
user = s.query(User).filter_by(id=user_id).first()
|
||||
if user:
|
||||
return user
|
||||
else:
|
||||
# User ID in session does not exist in the database; remove it
|
||||
del request.session["user_id"]
|
||||
|
||||
# No valid user in session; create a guest user
|
||||
# Generate a unique guest username
|
||||
while True:
|
||||
suffix = "".join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||||
guest_username = f"Guest-{suffix}"
|
||||
existing_user = s.query(User).filter_by(username=guest_username).first()
|
||||
if not existing_user:
|
||||
break # Unique username found
|
||||
|
||||
# Generate UUID and short ID
|
||||
user_uuid = uuid.uuid4()
|
||||
user_id = str(user_uuid)
|
||||
short_id = uuid_to_short_id(user_uuid)
|
||||
|
||||
# Create a new guest user
|
||||
guest_user = User(
|
||||
id=user_id,
|
||||
short_id=short_id,
|
||||
email=None, # Guests don't have an email
|
||||
username=guest_username,
|
||||
is_verified=False,
|
||||
)
|
||||
s.add(guest_user)
|
||||
s.commit()
|
||||
|
||||
# Store the user ID in the session
|
||||
request.session["user_id"] = guest_user.id
|
||||
|
||||
return guest_user
|
||||
|
||||
def get_mime_type(filename):
|
||||
# Guess the MIME type based on the file extension
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
if not mime_type:
|
||||
mime_type = "application/octet-stream"
|
||||
return mime_type
|
||||
|
||||
|
||||
def uuid_to_short_id(u):
|
||||
"""Encode UUID to a URL-safe base64 string without padding."""
|
||||
return base64.urlsafe_b64encode(u.bytes).decode("ascii").rstrip("=")
|
||||
|
||||
def short_id_to_uuid(sid):
|
||||
"""Decode the short ID back to UUID, trying different padding lengths."""
|
||||
# Try with 0 to 3 padding characters
|
||||
for padding_length in range(5):
|
||||
try:
|
||||
padded = sid + ("=" * padding_length)
|
||||
bytes_data = base64.urlsafe_b64decode(padded)
|
||||
if len(bytes_data) == 16: # UUID is 16 bytes
|
||||
return uuid.UUID(bytes=bytes_data)
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# If we get here, none of the padding attempts worked
|
||||
log.error(f"Failed to convert short_id {sid} to UUID after trying all padding lengths")
|
||||
return None
|
||||
|
||||
def get_user_db_url(user_id):
|
||||
"""Return the database URL for the user's SQLite database."""
|
||||
APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
db_file = os.path.join(APP_DIR, f"user_{user_id}.db")
|
||||
return f"sqlite:///{db_file}"
|
||||
|
||||
def filesizeformat(value):
|
||||
"""Returns the human-readable file size."""
|
||||
for unit in ["bytes", "KB", "MB", "GB", "TB"]:
|
||||
if value < 1024.0:
|
||||
return f"{value:.2f} {unit}"
|
||||
value /= 1024.0
|
||||
return f"{value:.2f} PB"
|
||||
|
||||
################################################################################
|
||||
# Database Setup
|
||||
################################################################################
|
||||
|
||||
APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DB_URL = f"sqlite:///{os.path.join(APP_DIR, 'main.db')}"
|
||||
log.debug(f"Using database URL: {DB_URL}") # For debugging
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
id = Column(String, primary_key=True) # UUID
|
||||
short_id = Column(String, unique=True, nullable=False)
|
||||
email = Column(String, unique=True, nullable=True) # Guests don't have an email.
|
||||
username = Column(String, unique=True, nullable=False) # user-chosen handle
|
||||
code_hash = Column(String, nullable=True) # bcrypt hash of code
|
||||
code_expires = Column(DateTime, nullable=True) # time limit for code
|
||||
is_verified = Column(Boolean, default=False)
|
||||
enable_gravatar = Column(Boolean, default=False) # Gravatar support
|
||||
is_admin = Column(Boolean, default=False) # Admin flag
|
||||
|
||||
# Indexes for faster lookup
|
||||
__table_args__ = (
|
||||
Index("ix_users_id", "id"),
|
||||
Index("ix_users_short_id", "short_id"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(username='{self.username}', email='{self.email}')>"
|
||||
|
||||
class Media(Base):
|
||||
__tablename__ = "media"
|
||||
id = Column(String, primary_key=True) # UUID
|
||||
short_id = Column(String, unique=True, nullable=False)
|
||||
user_id = Column(String, nullable=False) # Owner's UUID
|
||||
filename = Column(String, nullable=False)
|
||||
title = Column(String, nullable=True) # Optional title
|
||||
media_type = Column(String, nullable=False) # 'image', 'audio', 'video'
|
||||
media_b64 = Column(String, nullable=False)
|
||||
upload_date = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
is_public = Column(Boolean, default=True)
|
||||
size = Column(Integer, nullable=False) # Size in bytes
|
||||
|
||||
# Indexes for faster lookup
|
||||
__table_args__ = (
|
||||
Index("ix_media_id", "id"),
|
||||
Index("ix_media_short_id", "short_id"),
|
||||
)
|
||||
|
||||
################################################################################
|
||||
# Main Database (for Users)
|
||||
################################################################################
|
||||
|
||||
engine = create_engine(DB_URL, echo=False)
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
################################################################################
|
||||
# Jinja2 Environment and Custom Filters
|
||||
################################################################################
|
||||
|
||||
@subscriber(IJinja2Environment)
|
||||
def add_jinja2_filters(event):
|
||||
env = event.environment
|
||||
env.filters["filesizeformat"] = filesizeformat
|
||||
|
||||
################################################################################
|
||||
# Routes
|
||||
################################################################################
|
||||
|
||||
@view_config(route_name="home", renderer="home.html.j2")
|
||||
def home_view(request):
|
||||
return {
|
||||
"request": request,
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Authentication Views
|
||||
################################################################################
|
||||
|
||||
@view_config(route_name="login", request_method="GET", renderer="login.html.j2")
|
||||
def login_get_view(request):
|
||||
return {"request": request}
|
||||
|
||||
@view_config(route_name="login", request_method="POST")
|
||||
def login_post_view(request):
|
||||
email = request.POST.get("email", "").strip().lower()
|
||||
if not email:
|
||||
return Response("Email required.", status=400)
|
||||
|
||||
session = request.dbsession
|
||||
user = session.query(User).filter_by(email=email).first()
|
||||
|
||||
if not user:
|
||||
# Generate UUID and short ID
|
||||
user_uuid = uuid.uuid4()
|
||||
user_id = str(user_uuid)
|
||||
short_id = uuid_to_short_id(user_uuid)
|
||||
# Create new user
|
||||
user = User(
|
||||
id=user_id,
|
||||
short_id=short_id,
|
||||
email=email,
|
||||
username=email.split("@")[0],
|
||||
is_verified=False,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
# Generate 6-digit code
|
||||
code_str = f"{random.randint(0,999999):06d}"
|
||||
code_hash = bcrypt.hashpw(code_str.encode("utf-8"), bcrypt.gensalt()).decode(
|
||||
"utf-8"
|
||||
)
|
||||
user.code_hash = code_hash
|
||||
user.code_expires = datetime.datetime.now() + datetime.timedelta(minutes=15)
|
||||
user.is_verified = False
|
||||
session.commit()
|
||||
|
||||
# Send code via email
|
||||
email_body = f"Your verification code is: {code_str}"
|
||||
send_email(user.email, "Your Verification Code", email_body)
|
||||
|
||||
# Store the email in the session for verification
|
||||
request.session["login_email"] = email
|
||||
|
||||
return HTTPFound(location=request.route_url("verify"))
|
||||
|
||||
@view_config(route_name="verify", request_method="GET", renderer="verify.html.j2")
|
||||
def verify_get_view(request):
|
||||
return {"request": request}
|
||||
|
||||
@view_config(route_name="verify", request_method="POST")
|
||||
def verify_post_view(request):
|
||||
code_entered = request.POST.get("code", "").strip()
|
||||
if not code_entered or len(code_entered) != 6:
|
||||
return Response("Invalid code.", status=400)
|
||||
|
||||
email = request.session.get("login_email")
|
||||
if not email:
|
||||
return Response(
|
||||
"No email found in session. Please start the login process again.",
|
||||
status=400,
|
||||
)
|
||||
|
||||
s = request.dbsession
|
||||
user = (
|
||||
s.query(User)
|
||||
.filter(
|
||||
User.email == email,
|
||||
User.code_expires > datetime.datetime.now(),
|
||||
User.code_hash != None,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not user:
|
||||
return Response("Code not found or expired.", status=400)
|
||||
|
||||
if not bcrypt.checkpw(code_entered.encode("utf-8"), user.code_hash.encode("utf-8")):
|
||||
return Response("Invalid code.", status=400)
|
||||
|
||||
user.is_verified = True
|
||||
user.code_hash = None
|
||||
user.code_expires = None
|
||||
s.commit()
|
||||
|
||||
# Remove the email from the session
|
||||
del request.session["login_email"]
|
||||
|
||||
request.session["user_id"] = user.id
|
||||
return HTTPFound(location=request.route_url("home"))
|
||||
|
||||
@view_config(route_name="logout")
|
||||
def logout_view(request):
|
||||
request.session.invalidate()
|
||||
return HTTPFound(location=request.route_url("home"))
|
||||
|
||||
################################################################################
|
||||
# Profile and Media Upload
|
||||
################################################################################
|
||||
|
||||
@view_config(route_name="profile", request_method="GET", renderer="profile.html.j2")
|
||||
def profile_get_view(request):
|
||||
user = request.user
|
||||
if not user:
|
||||
return Response("You must be logged in to access your profile.", status=403)
|
||||
gravatar_url = get_gravatar_url(user.email) if user.enable_gravatar else ""
|
||||
|
||||
# Calculate stats
|
||||
total_uploads = 0
|
||||
total_size = 0 # in bytes
|
||||
|
||||
user_db_url = get_user_db_url(user.id)
|
||||
db_file = os.path.join(APP_DIR, f"user_{user.id}.db")
|
||||
if os.path.exists(db_file):
|
||||
user_engine = create_engine(user_db_url)
|
||||
UserSession = sessionmaker(bind=user_engine)
|
||||
Base.metadata.create_all(user_engine)
|
||||
user_dbsession = UserSession()
|
||||
|
||||
media_items = user_dbsession.query(Media).all()
|
||||
total_uploads = len(media_items)
|
||||
total_size = sum(media.size for media in media_items)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"user": user,
|
||||
"gravatar_url": gravatar_url,
|
||||
"total_uploads": total_uploads,
|
||||
"total_size": total_size,
|
||||
}
|
||||
|
||||
@view_config(route_name="profile", request_method="POST")
|
||||
def profile_post_view(request):
|
||||
if not request.user:
|
||||
return Response("You must be logged in to update your profile.", status=403)
|
||||
if not request.user.is_verified:
|
||||
return Response(
|
||||
"This account is in guest mode, log in to update your profile.", status=403
|
||||
)
|
||||
s = request.dbsession
|
||||
enable_gravatar = request.POST.get("enable_gravatar") == "on"
|
||||
request.user.enable_gravatar = enable_gravatar
|
||||
|
||||
# Handle username update if provided
|
||||
new_username = request.POST.get("new_username", "").strip()
|
||||
if new_username:
|
||||
# Check if the new username is already taken
|
||||
existing = s.query(User).filter(User.username == new_username).first()
|
||||
if existing and existing.id != request.user.id:
|
||||
return Response("Username is already in use.", status=400)
|
||||
request.user.username = new_username
|
||||
|
||||
s.commit()
|
||||
return HTTPFound(location=request.route_url("profile"))
|
||||
|
||||
@view_config(route_name="download_database")
|
||||
def download_database_view(request):
|
||||
user = request.user
|
||||
if not user or not user.is_verified:
|
||||
return Response("You must be logged in to download your database.", status=403)
|
||||
db_file = os.path.join(APP_DIR, f"user_{user.id}.db")
|
||||
if not os.path.exists(db_file):
|
||||
return Response("Database file not found.", status=404)
|
||||
with open(db_file, "rb") as f:
|
||||
data = f.read()
|
||||
response = Response(body=data, content_type="application/octet-stream")
|
||||
response.headers["Content-Disposition"] = f'attachment; filename="user_{user.id}.db"'
|
||||
return response
|
||||
|
||||
################################################################################
|
||||
# User Record Export (Users) and Import (Admins)
|
||||
################################################################################
|
||||
|
||||
@view_config(route_name="export_user_record")
|
||||
def export_user_record_view(request):
|
||||
user = request.user
|
||||
if not user or not user.is_verified:
|
||||
return Response("You must be logged in to export your user record.", status=403)
|
||||
|
||||
# Create a JSON representation of the user record
|
||||
user_record = {
|
||||
"id": user.id,
|
||||
"short_id": user.short_id,
|
||||
"email": user.email,
|
||||
"username": user.username,
|
||||
"enable_gravatar": user.enable_gravatar,
|
||||
"is_admin": user.is_admin,
|
||||
}
|
||||
|
||||
# Convert to JSON string
|
||||
user_json = json.dumps(user_record).encode("utf-8")
|
||||
|
||||
# Create response
|
||||
response = Response(body=user_json, content_type="application/json")
|
||||
response.headers[
|
||||
"Content-Disposition"
|
||||
] = 'attachment; filename="user_record.json"'
|
||||
return response
|
||||
|
||||
@view_config(route_name="import_user_record", request_method="GET", renderer="import_user_record.html.j2")
|
||||
@admin_required
|
||||
def import_user_record_get_view(request):
|
||||
return {"request": request}
|
||||
|
||||
@view_config(route_name="import_user_record", request_method="POST")
|
||||
@admin_required
|
||||
def import_user_record_post_view(request):
|
||||
# Admin-only import process
|
||||
|
||||
# Get the uploaded user record file
|
||||
user_record_file = request.POST.get("user_record_file")
|
||||
if user_record_file is None or not getattr(user_record_file, "filename", "").strip():
|
||||
return Response("No user record file uploaded.", status=400)
|
||||
|
||||
# Read and parse the JSON data
|
||||
try:
|
||||
user_record_data = user_record_file.file.read()
|
||||
user_record = json.loads(user_record_data)
|
||||
except Exception as e:
|
||||
return Response(f"Invalid user record file: {e}", status=400)
|
||||
|
||||
s = request.dbsession
|
||||
|
||||
# Check if user already exists
|
||||
existing_user = s.query(User).filter_by(id=user_record["id"]).first()
|
||||
if existing_user:
|
||||
return Response("User already exists in the database.", status=400)
|
||||
|
||||
# Create new user with provided data
|
||||
user = User(
|
||||
id=user_record["id"],
|
||||
short_id=user_record["short_id"],
|
||||
email=user_record["email"],
|
||||
username=user_record["username"],
|
||||
enable_gravatar=user_record.get("enable_gravatar", False),
|
||||
is_admin=user_record.get("is_admin", False),
|
||||
is_verified=True, # Assume verified
|
||||
)
|
||||
s.add(user)
|
||||
s.commit()
|
||||
|
||||
request.session.flash(f"User {user.username} imported successfully.")
|
||||
return HTTPFound(location=request.route_url("home"))
|
||||
|
||||
################################################################################
|
||||
# Media Upload, Listing, and Management
|
||||
################################################################################
|
||||
|
||||
@view_config(
|
||||
route_name="upload_media", request_method="GET", renderer="upload_media.html.j2"
|
||||
)
|
||||
def upload_media_get_view(request):
|
||||
user = request.user
|
||||
if not user or not user.is_verified:
|
||||
return Response("You must be logged in to upload media.", status=403)
|
||||
return {"request": request}
|
||||
|
||||
@view_config(route_name="upload_media", request_method="POST")
|
||||
def upload_media_post_view(request):
|
||||
user = request.user
|
||||
if not user or not user.is_verified:
|
||||
return Response("You must be logged in to upload media.", status=403)
|
||||
|
||||
media_file = request.POST.get("media_file")
|
||||
if media_file is None or not getattr(media_file, "filename", "").strip():
|
||||
return Response("No file uploaded.", status=400)
|
||||
|
||||
raw_bytes = media_file.file.read()
|
||||
max_size = 30 * 1024 * 1024 # 30 MB
|
||||
if len(raw_bytes) > max_size:
|
||||
return Response("File size exceeds the 30MB limit.", status=400)
|
||||
|
||||
file_size = len(raw_bytes) # Store the size in bytes
|
||||
|
||||
# Determine media type based on MIME type
|
||||
filename = media_file.filename
|
||||
mime_type = get_mime_type(filename)
|
||||
if not mime_type.startswith(("image/", "audio/", "video/")):
|
||||
return Response("Unsupported media type.", status=400)
|
||||
|
||||
media_type = mime_type.split("/")[0]
|
||||
|
||||
# Get title from form
|
||||
title = request.POST.get("title", "").strip()
|
||||
|
||||
# Encode content to base64
|
||||
encoded_str = base64.b64encode(raw_bytes).decode("utf-8")
|
||||
|
||||
# Handle user's database
|
||||
user_db_url = get_user_db_url(user.id)
|
||||
user_engine = create_engine(user_db_url)
|
||||
UserSession = sessionmaker(bind=user_engine)
|
||||
Base.metadata.create_all(user_engine)
|
||||
user_dbsession = UserSession()
|
||||
|
||||
# Generate UUID and short ID for the media
|
||||
media_uuid = uuid.uuid4()
|
||||
media_id = str(media_uuid)
|
||||
media_short_id = uuid_to_short_id(media_uuid)
|
||||
|
||||
is_public = request.POST.get("is_public") == "on"
|
||||
|
||||
media = Media(
|
||||
id=media_id,
|
||||
short_id=media_short_id,
|
||||
user_id=user.id,
|
||||
filename=filename,
|
||||
title=title,
|
||||
media_type=media_type,
|
||||
media_b64=encoded_str,
|
||||
is_public=is_public,
|
||||
size=file_size,
|
||||
)
|
||||
user_dbsession.add(media)
|
||||
user_dbsession.commit()
|
||||
|
||||
return HTTPFound(
|
||||
location=request.route_url(
|
||||
"view_media_details",
|
||||
user_short_id=user.short_id,
|
||||
media_short_id=media.short_id,
|
||||
)
|
||||
)
|
||||
|
||||
@view_config(route_name="list_media", renderer="list_media.html.j2")
|
||||
def list_media_view(request):
|
||||
# Aggregate public media from all users
|
||||
s = request.dbsession
|
||||
users = s.query(User).filter(User.is_verified == True).all()
|
||||
media_list = []
|
||||
for user in users:
|
||||
user_db_url = get_user_db_url(user.id)
|
||||
db_file = os.path.join(APP_DIR, f"user_{user.id}.db")
|
||||
if not os.path.exists(db_file):
|
||||
continue
|
||||
user_engine = create_engine(user_db_url)
|
||||
UserSession = sessionmaker(bind=user_engine)
|
||||
Base.metadata.create_all(user_engine)
|
||||
user_dbsession = UserSession()
|
||||
|
||||
media_items = user_dbsession.query(Media).filter(Media.is_public == True).all()
|
||||
for media in media_items:
|
||||
media_list.append(
|
||||
{
|
||||
"media": media,
|
||||
"username": user.username,
|
||||
"user_short_id": user.short_id,
|
||||
}
|
||||
)
|
||||
|
||||
# Sort media by upload date (recent first)
|
||||
media_list.sort(key=lambda x: x["media"].upload_date, reverse=True)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"media_list": media_list,
|
||||
}
|
||||
|
||||
@view_config(route_name="user_media", renderer="user_media.html.j2")
|
||||
def user_media_view(request):
|
||||
# View uploads by a particular user
|
||||
user_short_id = request.matchdict.get("user_short_id")
|
||||
log.debug(f"Looking up user with short_id: {user_short_id}") # Debug
|
||||
|
||||
try:
|
||||
# First convert short_id to UUID
|
||||
user_uuid = short_id_to_uuid(user_short_id)
|
||||
if not user_uuid:
|
||||
log.error(f"Could not convert short_id {user_short_id} to UUID")
|
||||
return Response("User not found.", status=404)
|
||||
|
||||
s = request.dbsession
|
||||
user = s.query(User).filter_by(id=str(user_uuid)).first()
|
||||
log.debug(f"User found: {user}") # Debug
|
||||
|
||||
if not user:
|
||||
return Response("User not found.", status=404)
|
||||
|
||||
viewer = request.user
|
||||
is_owner = viewer and viewer.id == user.id
|
||||
|
||||
user_db_url = get_user_db_url(user.id)
|
||||
db_file = os.path.join(APP_DIR, f"user_{user.id}.db")
|
||||
if not os.path.exists(db_file):
|
||||
return Response("User has no uploads.", status=404)
|
||||
user_engine = create_engine(user_db_url)
|
||||
UserSession = sessionmaker(bind=user_engine)
|
||||
Base.metadata.create_all(user_engine)
|
||||
user_dbsession = UserSession()
|
||||
|
||||
if is_owner:
|
||||
# Show all media (public and private)
|
||||
media_items = user_dbsession.query(Media).all()
|
||||
else:
|
||||
# Show only public media
|
||||
media_items = user_dbsession.query(Media).filter(Media.is_public == True).all()
|
||||
|
||||
# Sort media by upload date (recent first)
|
||||
media_items.sort(key=lambda media: media.upload_date, reverse=True)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"media_items": media_items,
|
||||
"user": user,
|
||||
"is_owner": is_owner,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
log.exception(f"Error processing user_short_id {user_short_id}")
|
||||
return Response("Error processing request.", status=500)
|
||||
|
||||
@view_config(route_name="view_media_details", renderer="view_media_details.html.j2")
|
||||
def view_media_details_view(request):
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
user_short_id = request.matchdict.get("user_short_id")
|
||||
if not user_short_id or not media_short_id:
|
||||
return Response("Invalid request.", status=400)
|
||||
|
||||
s = request.dbsession
|
||||
user = s.query(User).filter_by(short_id=user_short_id).first()
|
||||
if not user:
|
||||
return Response("User not found.", status=404)
|
||||
user_id = user.id
|
||||
|
||||
user_db_url = get_user_db_url(user_id)
|
||||
db_file = os.path.join(APP_DIR, f"user_{user_id}.db")
|
||||
if not os.path.exists(db_file):
|
||||
return Response("User database not found.", status=404)
|
||||
user_engine = create_engine(user_db_url)
|
||||
UserSession = sessionmaker(bind=user_engine)
|
||||
user_dbsession = UserSession()
|
||||
|
||||
# Lookup media by short_id
|
||||
media = user_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
viewer = request.user
|
||||
is_owner = viewer and viewer.id == user_id
|
||||
|
||||
if not media.is_public and not is_owner:
|
||||
return Response("Media not available.", status=403)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"media": media,
|
||||
"username": user.username,
|
||||
"user_short_id": user.short_id,
|
||||
"is_owner": is_owner,
|
||||
}
|
||||
|
||||
@view_config(route_name="delete_media", request_method="POST")
|
||||
def delete_media_view(request):
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
user_short_id = request.matchdict.get("user_short_id")
|
||||
|
||||
viewer = request.user
|
||||
if not viewer or not viewer.is_verified:
|
||||
return Response("You must be logged in.", status=403)
|
||||
|
||||
if viewer.short_id != user_short_id:
|
||||
return Response("You are not authorized to delete this media.", status=403)
|
||||
|
||||
user_db_url = get_user_db_url(viewer.id)
|
||||
user_engine = create_engine(user_db_url)
|
||||
UserSession = sessionmaker(bind=user_engine)
|
||||
user_dbsession = UserSession()
|
||||
|
||||
media = user_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Delete the media
|
||||
user_dbsession.delete(media)
|
||||
user_dbsession.commit()
|
||||
|
||||
return HTTPFound(
|
||||
location=request.route_url("user_media", user_short_id=viewer.short_id)
|
||||
)
|
||||
|
||||
@view_config(route_name="edit_media", request_method="GET", renderer="edit_media.html.j2")
|
||||
def edit_media_get_view(request):
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
user_short_id = request.matchdict.get("user_short_id")
|
||||
|
||||
viewer = request.user
|
||||
if not viewer or not viewer.is_verified:
|
||||
return Response("You must be logged in.", status=403)
|
||||
|
||||
if viewer.short_id != user_short_id:
|
||||
return Response("You are not authorized to edit this media.", status=403)
|
||||
|
||||
user_db_url = get_user_db_url(viewer.id)
|
||||
user_engine = create_engine(user_db_url)
|
||||
UserSession = sessionmaker(bind=user_engine)
|
||||
user_dbsession = UserSession()
|
||||
|
||||
media = user_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"media": media,
|
||||
}
|
||||
|
||||
@view_config(route_name="edit_media", request_method="POST")
|
||||
def edit_media_post_view(request):
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
user_short_id = request.matchdict.get("user_short_id")
|
||||
|
||||
viewer = request.user
|
||||
if not viewer or not viewer.is_verified:
|
||||
return Response("You must be logged in.", status=403)
|
||||
|
||||
if viewer.short_id != user_short_id:
|
||||
return Response("You are not authorized to edit this media.", status=403)
|
||||
|
||||
user_db_url = get_user_db_url(viewer.id)
|
||||
user_engine = create_engine(user_db_url)
|
||||
UserSession = sessionmaker(bind=user_engine)
|
||||
user_dbsession = UserSession()
|
||||
|
||||
media = user_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
# Handle title update
|
||||
new_title = request.POST.get("title", "").strip()
|
||||
media.title = new_title
|
||||
|
||||
# Handle media file update
|
||||
new_media_file = request.POST.get("media_file")
|
||||
if new_media_file and getattr(new_media_file, "filename", "").strip():
|
||||
raw_bytes = new_media_file.file.read()
|
||||
max_size = 30 * 1024 * 1024 # 30 MB
|
||||
if len(raw_bytes) > max_size:
|
||||
return Response("File size exceeds the 30MB limit.", status=400)
|
||||
file_size = len(raw_bytes)
|
||||
filename = new_media_file.filename
|
||||
mime_type = get_mime_type(filename)
|
||||
if not mime_type.startswith(("image/", "audio/", "video/")):
|
||||
return Response("Unsupported media type.", status=400)
|
||||
media_type = mime_type.split("/")[0]
|
||||
encoded_str = base64.b64encode(raw_bytes).decode("utf-8")
|
||||
|
||||
# Update media fields
|
||||
media.filename = filename
|
||||
media.media_type = media_type
|
||||
media.media_b64 = encoded_str
|
||||
media.size = file_size
|
||||
|
||||
# Handle public/private update
|
||||
is_public = request.POST.get("is_public") == "on"
|
||||
media.is_public = is_public
|
||||
|
||||
user_dbsession.commit()
|
||||
|
||||
return HTTPFound(
|
||||
location=request.route_url(
|
||||
"view_media_details",
|
||||
user_short_id=viewer.short_id,
|
||||
media_short_id=media.short_id,
|
||||
)
|
||||
)
|
||||
|
||||
################################################################################
|
||||
# Media Viewing and Downloading
|
||||
################################################################################
|
||||
|
||||
@view_config(route_name="view_media")
|
||||
def view_media_view(request):
|
||||
media_short_id = request.matchdict.get("media_short_id")
|
||||
user_short_id = request.matchdict.get("user_short_id")
|
||||
if not user_short_id or not media_short_id:
|
||||
return Response("Invalid request.", status=400)
|
||||
|
||||
s = request.dbsession
|
||||
user = s.query(User).filter_by(short_id=user_short_id).first()
|
||||
if not user:
|
||||
return Response("User not found.", status=404)
|
||||
user_id = user.id
|
||||
|
||||
user_db_url = get_user_db_url(user_id)
|
||||
db_file = os.path.join(APP_DIR, f"user_{user_id}.db")
|
||||
if not os.path.exists(db_file):
|
||||
return Response("User database not found.", status=404)
|
||||
user_engine = create_engine(user_db_url)
|
||||
UserSession = sessionmaker(bind=user_engine)
|
||||
user_dbsession = UserSession()
|
||||
|
||||
# Lookup media by short_id
|
||||
media = user_dbsession.query(Media).filter_by(short_id=media_short_id).first()
|
||||
if not media:
|
||||
return Response("Media not found.", status=404)
|
||||
|
||||
viewer = request.user
|
||||
is_owner = viewer and viewer.id == user.id
|
||||
|
||||
if not media.is_public and not is_owner:
|
||||
return Response("Media not available.", status=403)
|
||||
|
||||
# Decode base64 content
|
||||
media_data = base64.b64decode(media.media_b64)
|
||||
mime_type = get_mime_type(media.filename)
|
||||
|
||||
# Prepare filename for download
|
||||
if media.title:
|
||||
# Use title as filename, slugify it
|
||||
file_extension = os.path.splitext(media.filename)[1]
|
||||
download_filename = f"{slugify(media.title)}{file_extension}"
|
||||
else:
|
||||
# Use original filename
|
||||
download_filename = media.filename
|
||||
|
||||
# Serve the media content with appropriate headers
|
||||
response = Response(body=media_data, content_type=mime_type)
|
||||
response.headers.update(
|
||||
{
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Disposition": f'inline; filename="{download_filename}"',
|
||||
}
|
||||
)
|
||||
return response
|
||||
|
||||
################################################################################
|
||||
# Main
|
||||
################################################################################
|
||||
|
||||
def main(global_config=None, **settings):
|
||||
from pyramid.decorator import reify
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Set up the session factory with the specified settings
|
||||
session_factory = SignedCookieSessionFactory(
|
||||
secret="it-is-a-secret-you-must-change",
|
||||
hashalg="sha512",
|
||||
timeout=31104000, # Approx. one year in seconds
|
||||
max_age=31104000, # Set Max-Age attribute on cookie
|
||||
reissue_time=15552000, # Approx. six months
|
||||
samesite=None, # Allows cross-site requests if needed
|
||||
httponly=True, # Helps mitigate XSS attacks
|
||||
secure=False, # Set to True if using HTTPS
|
||||
)
|
||||
|
||||
if not settings:
|
||||
settings = {}
|
||||
settings["sqlalchemy.url"] = DB_URL
|
||||
|
||||
config = Configurator(settings=settings, session_factory=session_factory)
|
||||
config.include("pyramid_jinja2")
|
||||
|
||||
# Add .html.j2 extension for Jinja2 templates
|
||||
# Set up Jinja2 template search path
|
||||
config.add_jinja2_renderer(".j2")
|
||||
config.add_jinja2_search_path("templates", name=".j2")
|
||||
|
||||
# The Jinja2 filters are added via the event subscriber above
|
||||
|
||||
# Add user to all requests.
|
||||
config.add_request_method(callable=get_current_user, name="user", reify=True)
|
||||
|
||||
def dbsession(request):
|
||||
return Session()
|
||||
|
||||
config.add_request_method(dbsession, "dbsession", reify=True)
|
||||
|
||||
# Routes
|
||||
config.add_route("home", "/")
|
||||
|
||||
# Auth
|
||||
config.add_route("login", "/auth/login")
|
||||
config.add_route("verify", "/auth/verify")
|
||||
config.add_route("logout", "/auth/logout")
|
||||
config.add_route("profile", "/auth/profile")
|
||||
|
||||
# Export and Import User Record (Admin only for import)
|
||||
config.add_route("export_user_record", "/auth/export_user_record")
|
||||
config.add_route("import_user_record", "/admin/import_user_record")
|
||||
|
||||
# Media - Reordered routes with most specific first
|
||||
config.add_route("upload_media", "/media/upload")
|
||||
config.add_route("list_media", "/media/list")
|
||||
config.add_route("user_media", "/media/user/{user_short_id}") # Moved earlier
|
||||
config.add_route("view_media_details", "/media/{user_short_id}/{media_short_id}/details")
|
||||
config.add_route("edit_media", "/media/{user_short_id}/{media_short_id}/edit")
|
||||
config.add_route("delete_media", "/media/{user_short_id}/{media_short_id}/delete")
|
||||
config.add_route("view_media", "/media/{user_short_id}/{media_short_id}")
|
||||
|
||||
# Database Download
|
||||
config.add_route("download_database", "/auth/download_db")
|
||||
|
||||
config.scan()
|
||||
return config.make_wsgi_app()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = main()
|
||||
log.info("Serving on http://localhost:6544")
|
||||
serve(app, host="0.0.0.0", port=6544)
|
||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
plaster_pastedeploy
|
||||
pyramid
|
||||
pyramid_jinja2
|
||||
pyramid_debugtoolbar
|
||||
waitress
|
||||
pyramid_retry
|
||||
|
||||
bcrypt
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
164
templates/base.html.j2
Normal file
164
templates/base.html.j2
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Media Hosting App{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://www.unturf.com/css/pico.classless.min.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Base Styles */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Navigation Styles */
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
nav a strong {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* Navigation Links */
|
||||
nav ul {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
justify-content: end;
|
||||
gap: 1em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Hamburger Menu Button */
|
||||
#mobile-menu-button {
|
||||
display: none;
|
||||
font-size: 1.5em;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media only screen and (max-width: 600px) {
|
||||
/* Adjust nav grid to include hamburger button */
|
||||
nav {
|
||||
grid-template-columns: auto 1fr auto;
|
||||
}
|
||||
|
||||
/* Hide navigation links on mobile */
|
||||
nav ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show hamburger menu button on mobile */
|
||||
#mobile-menu-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Menu Modal Styles */
|
||||
#mobile-menu-dialog nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
#mobile-menu-dialog nav ul li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#mobile-menu-dialog button#mobile-menu-close {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 1em;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mobile-menu-dialog article {
|
||||
position: relative;
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="{{ request.route_url('home') }}"><strong>upload.unturf.com</strong></a>
|
||||
<!-- Hamburger Menu Button (only visible on mobile) -->
|
||||
<button id="mobile-menu-button" aria-label="Open Menu">☰</button>
|
||||
<!-- Navigation Links -->
|
||||
<ul>
|
||||
<li><a href="{{ request.route_url('list_media') }}">Browse Media</a></li>
|
||||
{% if request.user and request.user.is_verified %}
|
||||
<li><a href="{{ request.route_url('upload_media') }}">Upload Media</a></li>
|
||||
<li><a href="{{ request.route_url('profile') }}">Profile</a></li>
|
||||
<li><a href="{{ request.route_url('logout') }}">Logout</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ request.route_url('login') }}">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile Menu Modal -->
|
||||
<dialog id="mobile-menu-dialog">
|
||||
<article>
|
||||
<button aria-label="Close Menu" id="mobile-menu-close">✕</button>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ request.route_url('list_media') }}">Browse Media</a></li>
|
||||
{% if request.user and request.user.is_verified %}
|
||||
<li><a href="{{ request.route_url('upload_media') }}">Upload Media</a></li>
|
||||
<li><a href="{{ request.route_url('profile') }}">Profile</a></li>
|
||||
<li><a href="{{ request.route_url('logout') }}">Logout</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ request.route_url('login') }}">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</article>
|
||||
</dialog>
|
||||
|
||||
<!-- Flash messages -->
|
||||
{% if request.session.peek_flash() %}
|
||||
{% for message in request.session.pop_flash() %}
|
||||
<div class="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<main class="container">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- JavaScript for Mobile Menu -->
|
||||
<script>
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenuDialog = document.getElementById('mobile-menu-dialog');
|
||||
const mobileMenuClose = document.getElementById('mobile-menu-close');
|
||||
|
||||
mobileMenuButton.addEventListener('click', () => {
|
||||
mobileMenuDialog.showModal();
|
||||
});
|
||||
|
||||
mobileMenuClose.addEventListener('click', () => {
|
||||
mobileMenuDialog.close();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
35
templates/edit_media.html.j2
Normal file
35
templates/edit_media.html.j2
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Edit Media{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Edit Media</h1>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<label for="title">Title (optional):</label>
|
||||
<input type="text" name="title" value="{{ media.title }}" placeholder="Enter a title for the media"><br><br>
|
||||
|
||||
<label for="media_file">Replace Media File (leave blank to keep current):</label>
|
||||
<input type="file" name="media_file" accept="image/*,audio/*,video/*"><br><br>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="is_public" {% if media.is_public %}checked{% endif %}>
|
||||
Make Media Public
|
||||
</label><br><br>
|
||||
|
||||
<button type="submit">Update</button>
|
||||
</form>
|
||||
|
||||
<p>
|
||||
<!-- View and Download links -->
|
||||
<a href="{{ request.route_url('view_media', user_short_id=request.user.short_id, media_short_id=media.short_id) }}">View</a> |
|
||||
<a href="{{ request.route_url('view_media', user_short_id=request.user.short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a> |
|
||||
<a href="{{ request.route_url('view_media_details', user_short_id=request.user.short_id, media_short_id=media.short_id) }}">Back to Media Details</a>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<form action="{{ request.route_url('delete_media', user_short_id=request.user.short_id, media_short_id=media.short_id) }}" method="post">
|
||||
<button type="submit" onclick="return confirm('Are you sure you want to delete this media?');" style="border: 0px; background-color: #AA0000; color: white;">Delete</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
15
templates/home.html.j2
Normal file
15
templates/home.html.j2
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Welcome to the Media Hosting App</h1>
|
||||
<p>
|
||||
{% if request.user and request.user.is_verified %}
|
||||
Hello, {{ request.user.username }}!
|
||||
{% else %}
|
||||
Welcome, Guest!
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>This application allows authenticated users to upload images, audio, and short videos. Guests can browse and download publicly available media.</p>
|
||||
{% endblock %}
|
||||
12
templates/import_user_record.html.j2
Normal file
12
templates/import_user_record.html.j2
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Import User Record{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Import User Record</h1>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<label for="user_record_file">Select User Record JSON File:</label>
|
||||
<input type="file" name="user_record_file" accept="application/json" required><br><br>
|
||||
<button type="submit">Import</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
30
templates/list_media.html.j2
Normal file
30
templates/list_media.html.j2
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Media List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Media List</h1>
|
||||
|
||||
{% if media_list %}
|
||||
<ul>
|
||||
{% for media_item in media_list %}
|
||||
{% set media = media_item.media %}
|
||||
<li>
|
||||
<strong>{{ media.title if media.title else media.filename }}</strong><br>
|
||||
Uploaded by: <a href="{{ request.route_url('user_media', user_short_id=media_item.user_short_id) }}">{{ media_item.username }}</a><br>
|
||||
<!-- View and Download links -->
|
||||
<a href="{{ request.route_url('view_media', user_short_id=media_item.user_short_id, media_short_id=media.short_id) }}">View</a> |
|
||||
<a href="{{ request.route_url('view_media', user_short_id=media_item.user_short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a> |
|
||||
<!-- Link to media detail page -->
|
||||
<a href="{{ request.route_url('view_media_details', user_short_id=media_item.user_short_id, media_short_id=media.short_id) }}">View Details</a>
|
||||
{% if request.user and request.user.id == media.user_id %}
|
||||
| <a href="{{ request.route_url('edit_media', user_short_id=media_item.user_short_id, media_short_id=media.short_id) }}">Edit</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No media found.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
13
templates/login.html.j2
Normal file
13
templates/login.html.j2
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Login</h1>
|
||||
<p>Enter your email to receive a 6-digit verification code.</p>
|
||||
<form method="POST">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" name="email" required>
|
||||
<button type="submit">Get Verification Code</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
37
templates/profile.html.j2
Normal file
37
templates/profile.html.j2
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Profile{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Your Profile</h1>
|
||||
{% if gravatar_url %}
|
||||
<img src="{{ gravatar_url }}" alt="Gravatar Image">
|
||||
{% endif %}
|
||||
<p>Username: {{ user.username or "Not set" }}</p>
|
||||
|
||||
<form method="POST">
|
||||
<label>
|
||||
<input type="checkbox" name="enable_gravatar" {% if user.enable_gravatar %}checked{% endif %}>
|
||||
Enable Gravatar
|
||||
</label><br><br>
|
||||
|
||||
<label>
|
||||
New Username:
|
||||
<input type="text" name="new_username" placeholder="Enter new username">
|
||||
</label><br><br>
|
||||
|
||||
<button type="submit">Update Profile</button>
|
||||
</form>
|
||||
|
||||
<h2>Your Stats</h2>
|
||||
<ul>
|
||||
<li>Total Uploads: {{ total_uploads }}</li>
|
||||
<li>Total Storage Used: {{ total_size | filesizeformat }}</li>
|
||||
</ul>
|
||||
|
||||
<h2>Your Media</h2>
|
||||
<p><a href="{{ request.route_url('user_media', user_short_id=user.short_id) }}">View Your Uploads</a></p>
|
||||
<p><a href="{{ request.route_url('upload_media') }}">Upload New Media</a></p>
|
||||
<p><a href="{{ request.route_url('download_database') }}">Download Your Database</a></p>
|
||||
|
||||
{% endblock %}
|
||||
22
templates/upload_media.html.j2
Normal file
22
templates/upload_media.html.j2
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Upload Media{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Upload Media</h1>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<label for="media_file">Select Media File (Max 30MB):</label>
|
||||
<input type="file" name="media_file" accept="image/*,audio/*,video/*" required><br><br>
|
||||
|
||||
<label for="title">Title (optional):</label>
|
||||
<input type="text" name="title" placeholder="Enter a title for the media"><br><br>
|
||||
|
||||
<!-- Optionally, allow users to set media as public or private -->
|
||||
<label>
|
||||
<input type="checkbox" name="is_public" checked>
|
||||
Make Media Public
|
||||
</label><br><br>
|
||||
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
33
templates/user_media.html.j2
Normal file
33
templates/user_media.html.j2
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}{{ user.username }}'s Media{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ user.username }}'s Media</h1>
|
||||
|
||||
{% if media_items %}
|
||||
<ul>
|
||||
{% for media in media_items %}
|
||||
<li>
|
||||
<strong>{{ media.title if media.title else media.filename }}</strong><br>
|
||||
{% if media.is_public %}
|
||||
<span>Status: Public</span>
|
||||
{% else %}
|
||||
<span>Status: Private</span>
|
||||
{% endif %}<br>
|
||||
<!-- View and Download links -->
|
||||
<a href="{{ request.route_url('view_media', user_short_id=user.short_id, media_short_id=media.short_id) }}">View</a> |
|
||||
<a href="{{ request.route_url('view_media', user_short_id=user.short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a> |
|
||||
<!-- Link to media detail page -->
|
||||
<a href="{{ request.route_url('view_media_details', user_short_id=user.short_id, media_short_id=media.short_id) }}">View Details</a>
|
||||
{% if request.user and request.user.id == media.user_id %}
|
||||
| <a href="{{ request.route_url('edit_media', user_short_id=user.short_id, media_short_id=media.short_id) }}">Edit</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No media found.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
13
templates/verify.html.j2
Normal file
13
templates/verify.html.j2
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Verify Account{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Verify Your Account</h1>
|
||||
<p>A 6-digit code has been sent to your email. Please enter it below to verify your account.</p>
|
||||
<form method="POST">
|
||||
<label for="code">Verification Code:</label>
|
||||
<input type="text" name="code" required pattern="\d{6}" minlength="6" maxlength="6" inputmode="numeric">
|
||||
<button type="submit">Verify</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
38
templates/view_media.html.j2
Normal file
38
templates/view_media.html.j2
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}View Media{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ media.title if media.title else media.filename }}</h1>
|
||||
<p>Uploaded by <a href="{{ request.route_url('user_media', user_short_id=user_short_id) }}">{{ username }}</a> on {{ media.upload_date.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
<p>Size: {{ media.size | filesizeformat }}</p>
|
||||
|
||||
<!-- Display media based on type -->
|
||||
{% if media.media_type == 'image' %}
|
||||
<img src="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}" alt="{{ media.filename }}">
|
||||
{% elif media.media_type == 'audio' %}
|
||||
<audio controls>
|
||||
<source src="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
{% elif media.media_type == 'video' %}
|
||||
<video controls width="600">
|
||||
<source src="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
{% endif %}
|
||||
|
||||
<!-- View and Download links -->
|
||||
<p>
|
||||
<a href="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">View</a> |
|
||||
<a href="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a>
|
||||
</p>
|
||||
|
||||
{% if is_owner %}
|
||||
<!-- Edit button for the owner -->
|
||||
<p>
|
||||
<a href="{{ request.route_url('edit_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">Edit</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
28
templates/view_media_details.html.j2
Normal file
28
templates/view_media_details.html.j2
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Media Details{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Media Details</h1>
|
||||
|
||||
<p><strong>Title:</strong> {{ media.title if media.title else 'Untitled' }}</p>
|
||||
<p><strong>Filename:</strong> {{ media.filename }}</p>
|
||||
<p><strong>Uploaded by:</strong> <a href="{{ request.route_url('user_media', user_short_id=user_short_id) }}">{{ username }}</a></p>
|
||||
<p><strong>Upload Date:</strong> {{ media.upload_date.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
<p><strong>Status:</strong> {{ 'Public' if media.is_public else 'Private' }}</p>
|
||||
<p><strong>Size:</strong> {{ media.size | filesizeformat }}</p>
|
||||
|
||||
<!-- View and Download links -->
|
||||
<p>
|
||||
<a href="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">View</a> |
|
||||
<a href="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a>
|
||||
</p>
|
||||
|
||||
{% if is_owner %}
|
||||
<!-- Edit button for the owner -->
|
||||
<p>
|
||||
<a href="{{ request.route_url('edit_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">Edit</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue