2026-05-17 13:36:02 +00:00
#!/usr/bin/env python3
"""
2026-05-20 20:33:44 +00:00
Forge Paste — The ultimate self - hosted pastebin .
Zero dependencies · Hastebin - compatible · Full REST API · Syntax highlighting
2026-05-17 13:36:02 +00:00
Usage :
2026-05-19 11:17:01 +00:00
python3 forge - paste . py [ - - port PORT ] [ - - host HOST ] [ - - db PATH ] [ - - max - age DAYS ]
2026-05-17 13:36:02 +00:00
API :
2026-05-20 20:33:44 +00:00
POST / api / paste — Create paste ( JSON body or raw text )
GET / api / paste / < slug > — Get paste ( JSON )
PUT / api / paste / < slug > — Update paste ( requires edit_token )
DELETE / api / paste / < slug > — Delete paste ( requires edit_token )
GET / api / raw / < slug > — Raw text output
GET / api / download / < slug > — Download as file
GET / api / pastes / recent — List recent pastes
GET / api / stats — Server stats
GET / api / languages — Supported language list
GET / api / health — Health check
POST / api / documents — Hastebin - compat create
GET / api / documents / < slug > — Hastebin - compat get
2026-05-17 13:36:02 +00:00
"""
2026-05-20 20:33:44 +00:00
import sqlite3 , string , random , os , json , argparse , time , hashlib , secrets , re , gzip
2026-05-17 13:36:02 +00:00
from http . server import HTTPServer , BaseHTTPRequestHandler
2026-05-20 20:33:44 +00:00
from urllib . parse import urlparse , parse_qs
2026-05-17 13:36:02 +00:00
from datetime import datetime
2026-05-20 20:33:44 +00:00
from io import BytesIO
from threading import Thread
import html as html_mod
2026-05-17 13:36:02 +00:00
2026-05-20 20:33:44 +00:00
# ── Configuration ───────────────────────────────────────────
2026-05-17 13:36:02 +00:00
DB_PATH = os . environ . get ( " FORGE_PASTE_DB " , " /var/lib/forge-paste/pastes.db " )
HOST = os . environ . get ( " FORGE_PASTE_HOST " , " 127.0.0.1 " )
PORT = int ( os . environ . get ( " FORGE_PASTE_PORT " , " 7890 " ) )
2026-05-20 20:33:44 +00:00
MAX_SIZE = 2 * 1024 * 1024 # 2MB
MAX_AGE_DAYS = int ( os . environ . get ( " FORGE_PASTE_MAX_AGE " , " 30 " ) )
_CLEANUP_INTERVAL = 3600
2026-05-19 11:17:01 +00:00
_last_cleanup = 0
2026-05-20 20:33:44 +00:00
VERSION = " 2.0.0 "
2026-05-17 13:36:02 +00:00
2026-05-20 20:33:44 +00:00
# ── Expiry presets (seconds) ────────────────────────────────
EXPIRY_PRESETS = {
" 10m " : 600 , " 1h " : 3600 , " 1d " : 86400 , " 7d " : 604800 ,
" 30d " : 2592000 , " 90d " : 7776000 , " 1y " : 31536000 , " never " : 0 ,
}
# ── Languages ───────────────────────────────────────────────
LANGUAGES = [
" plaintext " , " bash " , " c " , " cpp " , " csharp " , " css " , " dart " , " diff " , " dockerfile " ,
" elixir " , " erlang " , " go " , " graphql " , " haskell " , " html " , " ini " , " java " ,
" javascript " , " json " , " julia " , " kotlin " , " latex " , " lua " , " makefile " , " markdown " ,
" nginx " , " objectivec " , " ocaml " , " perl " , " php " , " powershell " , " properties " ,
" python " , " r " , " ruby " , " rust " , " scala " , " scss " , " shell " , " sql " , " swift " ,
" toml " , " typescript " , " xml " , " yaml " , " zig " ,
]
# Extensions → language mapping for auto-detect
EXT_MAP = {
" py " : " python " , " js " : " javascript " , " ts " : " typescript " , " rb " : " ruby " , " rs " : " rust " ,
" go " : " go " , " java " : " java " , " c " : " c " , " cpp " : " cpp " , " h " : " c " , " hpp " : " cpp " ,
" cs " : " csharp " , " php " : " php " , " sh " : " bash " , " bash " : " bash " , " zsh " : " bash " ,
" sql " : " sql " , " html " : " html " , " htm " : " html " , " css " : " css " , " scss " : " scss " ,
" json " : " json " , " xml " : " xml " , " yaml " : " yaml " , " yml " : " yaml " , " toml " : " toml " ,
" md " : " markdown " , " dockerfile " : " dockerfile " , " makefile " : " makefile " ,
" lua " : " lua " , " swift " : " swift " , " kt " : " kotlin " , " scala " : " scala " ,
" hs " : " haskell " , " ex " : " elixir " , " erl " : " erlang " , " r " : " r " , " jl " : " julia " ,
" dart " : " dart " , " zig " : " zig " , " ini " : " ini " , " conf " : " ini " , " cfg " : " ini " ,
" diff " : " diff " , " patch " : " diff " , " ps1 " : " powershell " ,
}
def detect_language ( content , filename = None ) :
""" Auto-detect language from filename extension or content heuristics. """
if filename :
ext = filename . rsplit ( " . " , 1 ) [ - 1 ] . lower ( ) if " . " in filename else " "
if ext in EXT_MAP :
return EXT_MAP [ ext ]
# Content heuristics
first = content [ : 200 ] . strip ( )
if first . startswith ( " #!/usr/bin/env python " ) or first . startswith ( " #!/usr/bin/python " ) :
return " python "
if first . startswith ( " #!/bin/bash " ) or first . startswith ( " #!/bin/sh " ) :
return " bash "
if first . startswith ( " #!/usr/bin/env node " ) :
return " javascript "
if first . startswith ( " <?php " ) :
return " php "
if first . startswith ( " <!DOCTYPE html " ) or first . startswith ( " <html " ) :
return " html "
if first . startswith ( " --- " ) and " \n " in first :
return " yaml "
if first . startswith ( " { " ) and ' " ' in first [ : 50 ] :
try :
json . loads ( content [ : 1000 ] if len ( content ) > 1000 else content )
return " json "
except Exception :
pass
if re . match ( r " ^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP) \ s " , first , re . I ) :
return " sql "
if re . match ( r " ^(FROM|RUN|CMD|COPY|ENV|EXPOSE|WORKDIR) \ s " , first , re . I ) :
return " dockerfile "
if " def " in first and " : " in first :
return " python "
if " function " in first or " const " in first or " let " in first :
return " javascript "
if re . match ( r " ^(diff|---| \ + \ + \ +|@@) \ s " , first ) :
return " diff "
return " plaintext "
# ── Database ────────────────────────────────────────────────
2026-05-17 13:36:02 +00:00
def get_db ( ) :
db = sqlite3 . connect ( DB_PATH )
db . row_factory = sqlite3 . Row
db . execute ( " PRAGMA journal_mode=WAL " )
db . execute ( " PRAGMA foreign_keys=ON " )
return db
def init_db ( ) :
os . makedirs ( os . path . dirname ( DB_PATH ) , exist_ok = True )
db = get_db ( )
db . execute ( """
CREATE TABLE IF NOT EXISTS pastes (
id INTEGER PRIMARY KEY AUTOINCREMENT ,
slug TEXT UNIQUE NOT NULL ,
2026-05-20 20:33:44 +00:00
title TEXT ,
2026-05-17 13:36:02 +00:00
content TEXT NOT NULL ,
language TEXT ,
created_at REAL NOT NULL DEFAULT ( unixepoch ( ) ) ,
2026-05-20 20:33:44 +00:00
updated_at REAL ,
2026-05-17 13:36:02 +00:00
views INTEGER NOT NULL DEFAULT 0 ,
2026-05-19 11:17:01 +00:00
ip TEXT ,
2026-05-20 20:33:44 +00:00
expires_at REAL ,
burn_after_read INTEGER NOT NULL DEFAULT 0 ,
edit_token TEXT ,
parent_slug TEXT ,
size INTEGER NOT NULL DEFAULT 0 ,
is_public INTEGER NOT NULL DEFAULT 1
2026-05-17 13:36:02 +00:00
)
""" )
2026-05-20 20:33:44 +00:00
# Migrations for existing installs
2026-05-19 11:17:01 +00:00
cols = [ row [ 1 ] for row in db . execute ( " PRAGMA table_info(pastes) " ) . fetchall ( ) ]
2026-05-20 20:33:44 +00:00
migrations = {
" expires_at " : " ALTER TABLE pastes ADD COLUMN expires_at REAL " ,
" burn_after_read " : " ALTER TABLE pastes ADD COLUMN burn_after_read INTEGER NOT NULL DEFAULT 0 " ,
" edit_token " : " ALTER TABLE pastes ADD COLUMN edit_token TEXT " ,
" parent_slug " : " ALTER TABLE pastes ADD COLUMN parent_slug TEXT " ,
" size " : " ALTER TABLE pastes ADD COLUMN size INTEGER NOT NULL DEFAULT 0 " ,
" is_public " : " ALTER TABLE pastes ADD COLUMN is_public INTEGER NOT NULL DEFAULT 1 " ,
" title " : " ALTER TABLE pastes ADD COLUMN title TEXT " ,
" updated_at " : " ALTER TABLE pastes ADD COLUMN updated_at REAL " ,
}
for col , sql in migrations . items ( ) :
if col not in cols :
db . execute ( sql )
# Backfill size
db . execute ( " UPDATE pastes SET size = LENGTH(content) WHERE size = 0 AND content IS NOT NULL " )
# Backfill expires_at
if MAX_AGE_DAYS > 0 :
db . execute ( " UPDATE pastes SET expires_at = created_at + ? WHERE expires_at IS NULL " , ( MAX_AGE_DAYS * 86400 , ) )
db . commit ( )
2026-05-20 20:19:49 +00:00
db . execute ( " CREATE INDEX IF NOT EXISTS idx_slug ON pastes(slug) " )
db . execute ( " CREATE INDEX IF NOT EXISTS idx_created ON pastes(created_at) " )
db . execute ( " CREATE INDEX IF NOT EXISTS idx_expires ON pastes(expires_at) " )
2026-05-20 20:33:44 +00:00
db . execute ( " CREATE INDEX IF NOT EXISTS idx_public ON pastes(is_public) " )
2026-05-20 20:19:49 +00:00
db . commit ( )
2026-05-17 13:36:02 +00:00
db . close ( )
def generate_slug ( length = 8 ) :
chars = string . ascii_lowercase + string . digits
return ' ' . join ( random . choices ( chars , k = length ) )
2026-05-19 11:17:01 +00:00
def cleanup_expired ( ) :
global _last_cleanup
now = time . time ( )
if now - _last_cleanup < _CLEANUP_INTERVAL :
return
_last_cleanup = now
db = get_db ( )
2026-05-20 20:33:44 +00:00
# Delete expired
db . execute ( " DELETE FROM pastes WHERE expires_at IS NOT NULL AND expires_at < ? " , ( now , ) )
2026-05-19 11:17:01 +00:00
db . commit ( )
db . close ( )
2026-05-20 20:33:44 +00:00
def create_paste ( content , title = None , language = None , ip = None , expires_in = None , burn = False , is_public = True , parent_slug = None ) :
cleanup_expired ( )
2026-05-17 13:36:02 +00:00
db = get_db ( )
slug = generate_slug ( )
while db . execute ( " SELECT 1 FROM pastes WHERE slug=? " , ( slug , ) ) . fetchone ( ) :
slug = generate_slug ( )
2026-05-20 20:33:44 +00:00
edit_token = secrets . token_urlsafe ( 24 )
2026-05-19 11:17:01 +00:00
now = time . time ( )
2026-05-20 20:33:44 +00:00
if expires_in is not None :
expires_at = now + expires_in if expires_in > 0 else None
elif MAX_AGE_DAYS > 0 :
expires_at = now + ( MAX_AGE_DAYS * 86400 )
else :
expires_at = None
if not language :
language = detect_language ( content )
size = len ( content . encode ( " utf-8 " ) )
2026-05-17 13:36:02 +00:00
db . execute (
2026-05-20 20:33:44 +00:00
""" INSERT INTO pastes (slug, title, content, language, created_at, views, ip,
expires_at , burn_after_read , edit_token , parent_slug , size , is_public )
VALUES ( ? , ? , ? , ? , ? , 0 , ? , ? , ? , ? , ? , ? , ? ) """ ,
( slug , title , content , language , now , ip , expires_at ,
1 if burn else 0 , edit_token , parent_slug , size , 1 if is_public else 0 )
2026-05-17 13:36:02 +00:00
)
db . commit ( )
db . close ( )
2026-05-20 20:33:44 +00:00
return slug , edit_token
2026-05-17 13:36:02 +00:00
2026-05-20 20:33:44 +00:00
def get_paste ( slug , increment_view = True ) :
2026-05-17 13:36:02 +00:00
db = get_db ( )
2026-05-19 11:17:01 +00:00
row = db . execute (
" SELECT * FROM pastes WHERE slug=? AND (expires_at IS NULL OR expires_at > ?) " ,
( slug , time . time ( ) ) ,
) . fetchone ( )
2026-05-20 20:33:44 +00:00
if not row :
db . close ( )
return None
paste = dict ( row )
if increment_view :
2026-05-17 13:36:02 +00:00
db . execute ( " UPDATE pastes SET views = views + 1 WHERE slug=? " , ( slug , ) )
db . commit ( )
2026-05-20 20:33:44 +00:00
# Handle burn after read
if paste . get ( " burn_after_read " ) and increment_view :
db . execute ( " DELETE FROM pastes WHERE slug=? " , ( slug , ) )
db . commit ( )
paste [ " _burned " ] = True
db . close ( )
return paste
def update_paste ( slug , content , title = None , language = None ) :
db = get_db ( )
now = time . time ( )
size = len ( content . encode ( " utf-8 " ) )
if not language :
language = detect_language ( content )
db . execute (
" UPDATE pastes SET content=?, title=?, language=?, updated_at=?, size=? WHERE slug=? " ,
( content , title , language , now , size , slug )
)
db . commit ( )
db . close ( )
def delete_paste ( slug ) :
db = get_db ( )
db . execute ( " DELETE FROM pastes WHERE slug=? " , ( slug , ) )
db . commit ( )
db . close ( )
def get_recent ( limit = 20 , offset = 0 ) :
db = get_db ( )
rows = db . execute (
""" SELECT slug, title, language, created_at, views, size, burn_after_read, expires_at
FROM pastes WHERE is_public = 1 AND ( expires_at IS NULL OR expires_at > ? )
ORDER BY created_at DESC LIMIT ? OFFSET ? """ ,
( time . time ( ) , limit , offset )
) . fetchall ( )
2026-05-17 13:36:02 +00:00
db . close ( )
2026-05-20 20:33:44 +00:00
return [ dict ( r ) for r in rows ]
2026-05-17 13:36:02 +00:00
def get_stats ( ) :
db = get_db ( )
2026-05-20 20:33:44 +00:00
total = db . execute ( " SELECT COUNT(*) FROM pastes " ) . fetchone ( ) [ 0 ]
total_views = db . execute ( " SELECT COALESCE(SUM(views),0) FROM pastes " ) . fetchone ( ) [ 0 ]
total_size = db . execute ( " SELECT COALESCE(SUM(size),0) FROM pastes " ) . fetchone ( ) [ 0 ]
active = db . execute (
" SELECT COUNT(*) FROM pastes WHERE expires_at IS NULL OR expires_at > ? " ,
( time . time ( ) , ) ) . fetchone ( ) [ 0 ]
2026-05-17 13:36:02 +00:00
db . close ( )
2026-05-20 20:33:44 +00:00
return { " total_pastes " : total , " active_pastes " : active , " total_views " : total_views ,
" total_size " : total_size , " version " : VERSION }
2026-05-17 13:36:02 +00:00
def time_ago ( ts ) :
seconds = int ( time . time ( ) - ts )
if seconds < 60 : return " just now "
if seconds < 3600 : return f " { seconds / / 60 } m ago "
if seconds < 86400 : return f " { seconds / / 3600 } h ago "
return f " { seconds / / 86400 } d ago "
2026-05-20 20:33:44 +00:00
def format_bytes ( b ) :
if b < 1024 : return f " { b } B "
if b < 1048576 : return f " { b / 1024 : .1f } KB "
return f " { b / 1048576 : .1f } MB "
2026-05-17 13:36:02 +00:00
# ── HTML Templates ──────────────────────────────────────────
2026-05-20 20:33:44 +00:00
ICON_SVG = ' <svg xmlns= " http://www.w3.org/2000/svg " viewBox= " 0 0 64 64 " width= " 28 " height= " 28 " ><rect width= " 64 " height= " 64 " rx= " 12 " fill= " #0a1628 " /><path d= " M17 35L47 35L50 33L48 31L16 31L14 33Z " fill= " #b8d4e3 " /><path d= " M24 35L24 40L21 45L17 45L17 50L47 50L47 45L43 45L40 40L40 35Z " fill= " #b8d4e3 " opacity= " 0.9 " /><path d= " M32 12C36 19 35 23 34 26Q33 28 32 29Q31 28 30 26C29 23 28 19 32 12Z " fill= " #2aa4dd " /><path d= " M27 17C29 21 28.5 24 27.5 26Q27 28 27 29Q26.5 27.5 26 26C25 24 24.5 21 27 17Z " fill= " #5bcaee " opacity= " 0.8 " /><path d= " M37 17C35 21 35.5 24 36.5 26Q37 28 37 29Q37.5 27.5 38 26C39 24 39.5 21 37 17Z " fill= " #5bcaee " opacity= " 0.8 " /></svg> '
FAVICON = " data:image/svg+xml,<svg xmlns= ' http://www.w3.org/2000/svg ' viewBox= ' 0 0 64 64 ' ><rect width= ' 64 ' height= ' 64 ' rx= ' 12 ' fill= ' %230a 1628 ' /><path d= ' M32 12C36 19 35 23 34 26Q33 28 32 29Q31 28 30 26C29 23 28 19 32 12Z ' fill= ' %232a a4dd ' /></svg> "
2026-05-17 13:36:02 +00:00
2026-05-20 20:33:44 +00:00
CSS = """
2026-05-17 13:36:02 +00:00
< style >
2026-05-20 20:33:44 +00:00
: root {
- - bg : #0a0f1a; --bg2: #0e1726; --bg3: #131f30; --border: #1a2d42;
- - accent : #2aa4dd; --accent2: #5bcaee; --accent-dim: rgba(42,164,221,0.15);
- - text : #e2e8f0; --text2: #94a3b8; --text3: #64748b; --text4: #475569;
- - green : #22c55e; --red: #ef4444; --orange: #f59e0b; --purple: #a78bfa;
- - font : - apple - system , BlinkMacSystemFont , ' Segoe UI ' , Inter , Roboto , sans - serif ;
- - mono : ' JetBrains Mono ' , ' Fira Code ' , ' Cascadia Code ' , Menlo , monospace ;
- - radius : 10 px ;
}
* { margin : 0 ; padding : 0 ; box - sizing : border - box }
body { background : var ( - - bg ) ; color : var ( - - text ) ; font - family : var ( - - font ) ; min - height : 100 vh ; display : flex ; flex - direction : column ; font - size : 14 px }
a { color : var ( - - accent ) ; text - decoration : none ; transition : color .15 s }
a : hover { color : var ( - - accent2 ) }
: : selection { background : var ( - - accent - dim ) ; color : var ( - - accent2 ) }
: : - webkit - scrollbar { width : 8 px ; height : 8 px }
: : - webkit - scrollbar - track { background : transparent }
: : - webkit - scrollbar - thumb { background : var ( - - border ) ; border - radius : 4 px }
: : - webkit - scrollbar - thumb : hover { background : var ( - - text4 ) }
/ * Header * /
. hdr { border - bottom : 1 px solid var ( - - border ) ; padding : 12 px 24 px ; display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; background : var ( - - bg2 ) ; position : sticky ; top : 0 ; z - index : 100 ; backdrop - filter : blur ( 12 px ) }
. hdr . left { display : flex ; align - items : center ; gap : 10 px ; min - width : 0 }
. hdr h1 { font - size : 18 px ; font - weight : 700 ; white - space : nowrap }
. hdr . slug { font - size : 11 px ; color : var ( - - text3 ) ; font - family : var ( - - mono ) ; overflow : hidden ; text - overflow : ellipsis }
. hdr nav { display : flex ; gap : 6 px ; align - items : center }
. hdr . meta { font - size : 11 px ; color : var ( - - text3 ) ; white - space : nowrap }
. hdr . sep { color : var ( - - border ) ; font - size : 11 px }
/ * Buttons * /
. btn { padding : 6 px 14 px ; border - radius : 7 px ; border : 1 px solid transparent ; cursor : pointer ; font - size : 13 px ; font - weight : 500 ; transition : all .15 s ; color : var ( - - text ) ; font - family : var ( - - font ) ; display : inline - flex ; align - items : center ; gap : 6 px ; white - space : nowrap ; text - decoration : none ; line - height : 1.4 }
. btn svg { width : 14 px ; height : 14 px ; flex - shrink : 0 }
. btn - p { background : var ( - - accent ) ; border - color : var ( - - accent ) ; color : #fff}
. btn - p : hover { background : var ( - - accent2 ) ; border - color : var ( - - accent2 ) ; color : #fff}
. btn - s { background : var ( - - bg3 ) ; border - color : var ( - - border ) }
. btn - s : hover { background : var ( - - border ) ; border - color : var ( - - text4 ) }
. btn - g { background : rgba ( 34 , 197 , 94 , .1 ) ; border - color : rgba ( 34 , 197 , 94 , .3 ) ; color : var ( - - green ) }
. btn - g : hover { background : rgba ( 34 , 197 , 94 , .2 ) }
. btn - r { background : rgba ( 239 , 68 , 68 , .1 ) ; border - color : rgba ( 239 , 68 , 68 , .3 ) ; color : var ( - - red ) }
. btn - r : hover { background : rgba ( 239 , 68 , 68 , .2 ) }
. btn : disabled { opacity : .35 ; cursor : not - allowed }
. btn - icon { padding : 6 px 8 px }
/ * Tags * /
. tag { display : inline - flex ; align - items : center ; gap : 4 px ; padding : 2 px 8 px ; border - radius : 5 px ; font - size : 11 px ; font - weight : 500 ; font - family : var ( - - mono ) }
. tag - lang { background : var ( - - accent - dim ) ; color : var ( - - accent2 ) }
. tag - burn { background : rgba ( 239 , 68 , 68 , .12 ) ; color : var ( - - red ) }
. tag - exp { background : rgba ( 245 , 158 , 11 , .12 ) ; color : var ( - - orange ) }
/ * Editor * /
. editor { flex : 1 ; display : flex ; flex - direction : column ; padding : 16 px ; gap : 12 px }
. editor . top - bar { display : flex ; gap : 8 px ; flex - wrap : wrap ; align - items : center }
. editor . top - bar input [ type = text ] { flex : 1 ; min - width : 200 px ; background : var ( - - bg3 ) ; color : var ( - - text ) ; font - family : var ( - - font ) ; font - size : 13 px ; padding : 7 px 12 px ; border - radius : 7 px ; border : 1 px solid var ( - - border ) ; outline : none ; transition : border .15 s }
. editor . top - bar input : focus { border - color : var ( - - accent ) }
. editor . top - bar input : : placeholder { color : var ( - - text4 ) }
. editor select { background : var ( - - bg3 ) ; color : var ( - - text2 ) ; font - family : var ( - - mono ) ; font - size : 12 px ; padding : 7 px 10 px ; border - radius : 7 px ; border : 1 px solid var ( - - border ) ; outline : none ; cursor : pointer ; appearance : none ; - webkit - appearance : none ; background - image : url ( " data:image/svg+xml, % 3Csvg xmlns= ' http://www.w3.org/2000/svg ' width= ' 10 ' height= ' 6 ' %3E % 3Cpath d= ' M0 0l5 6 5-6z ' fill= ' % 2364748b ' / %3E % 3C/svg %3E " ) ; background - repeat : no - repeat ; background - position : right 8 px center ; padding - right : 24 px }
. editor textarea { flex : 1 ; width : 100 % ; background : var ( - - bg2 ) ; color : var ( - - text ) ; font - family : var ( - - mono ) ; font - size : 13 px ; line - height : 1.65 ; padding : 20 px ; border - radius : var ( - - radius ) ; border : 1 px solid var ( - - border ) ; resize : none ; outline : none ; tab - size : 4 ; - moz - tab - size : 4 ; transition : border .15 s }
. editor textarea : focus { border - color : rgba ( 42 , 164 , 221 , .4 ) }
. editor textarea : : placeholder { color : var ( - - text4 ) }
. editor . bottom - bar { display : flex ; justify - content : space - between ; align - items : center ; gap : 12 px ; flex - wrap : wrap }
. editor . info { font - size : 12 px ; color : var ( - - text3 ) ; display : flex ; gap : 12 px ; align - items : center }
. editor . opts { display : flex ; gap : 8 px ; flex - wrap : wrap ; align - items : center }
/ * Checkbox * /
. chk { display : flex ; align - items : center ; gap : 6 px ; cursor : pointer ; font - size : 12 px ; color : var ( - - text2 ) ; user - select : none }
. chk input { display : none }
. chk . box { width : 16 px ; height : 16 px ; border - radius : 4 px ; border : 1.5 px solid var ( - - border ) ; display : flex ; align - items : center ; justify - content : center ; transition : all .15 s ; background : var ( - - bg3 ) }
. chk input : checked + . box { background : var ( - - accent ) ; border - color : var ( - - accent ) }
. chk input : checked + . box : : after { content : ' ✓ ' ; color : #fff;font-size:10px;font-weight:700}
/ * Viewer * /
. viewer { flex : 1 ; padding : 16 px ; display : flex ; flex - direction : column ; gap : 12 px }
. viewer . toolbar { display : flex ; justify - content : space - between ; align - items : center ; flex - wrap : wrap ; gap : 8 px }
. viewer . toolbar . tags { display : flex ; gap : 6 px ; flex - wrap : wrap ; align - items : center }
. code - wrap { flex : 1 ; background : var ( - - bg2 ) ; border - radius : var ( - - radius ) ; border : 1 px solid var ( - - border ) ; overflow : auto ; position : relative }
. code - wrap pre { margin : 0 ; padding : 20 px ; padding - left : 0 }
. code - wrap code { font - family : var ( - - mono ) ! important ; font - size : 13 px ! important ; line - height : 1.65 ! important ; background : transparent ! important }
. code - wrap . lines { display : flex }
. code - wrap . line - nums { user - select : none ; text - align : right ; padding : 20 px 0 ; padding - right : 16 px ; padding - left : 20 px ; color : var ( - - text4 ) ; font - family : var ( - - mono ) ; font - size : 13 px ; line - height : 1.65 ; border - right : 1 px solid var ( - - border ) ; position : sticky ; left : 0 ; background : var ( - - bg2 ) ; z - index : 1 }
. code - wrap . line - nums a { color : var ( - - text4 ) ; display : block }
. code - wrap . line - nums a : hover { color : var ( - - accent ) }
. code - wrap . line - nums a . active { color : var ( - - accent ) ; background : var ( - - accent - dim ) }
. code - wrap . code - body { padding : 20 px ; padding - left : 16 px ; flex : 1 ; overflow - x : auto }
. code - wrap . code - body pre { padding : 0 }
/ * Burn notice * /
. burn - notice { background : rgba ( 239 , 68 , 68 , .08 ) ; border : 1 px solid rgba ( 239 , 68 , 68 , .25 ) ; border - radius : var ( - - radius ) ; padding : 12 px 16 px ; display : flex ; align - items : center ; gap : 10 px ; font - size : 13 px ; color : var ( - - red ) }
. burn - notice svg { flex - shrink : 0 }
/ * Result page * /
. result { display : flex ; align - items : center ; justify - content : center ; flex : 1 ; padding : 24 px }
. result - card { max - width : 520 px ; width : 100 % ; background : var ( - - bg2 ) ; border : 1 px solid var ( - - border ) ; border - radius : 16 px ; padding : 40 px ; text - align : center }
. result - card . icon { font - size : 48 px ; margin - bottom : 16 px }
. result - card h2 { font - size : 20 px ; font - weight : 600 ; margin - bottom : 8 px }
. result - card . url - box { background : var ( - - bg ) ; border : 1 px solid var ( - - accent - dim ) ; border - radius : var ( - - radius ) ; padding : 16 px ; margin : 20 px 0 ; font - family : var ( - - mono ) ; word - break : break - all ; color : var ( - - accent ) ; font - size : 15 px }
. result - card . actions { display : flex ; gap : 8 px ; justify - content : center ; flex - wrap : wrap }
. result - card . sub { font - size : 12 px ; color : var ( - - text3 ) ; margin - top : 16 px }
. result - card . sub code { background : var ( - - bg3 ) ; padding : 2 px 6 px ; border - radius : 4 px ; font - size : 11 px }
/ * 404 * /
. notfound { display : flex ; align - items : center ; justify - content : center ; flex : 1 ; text - align : center ; padding : 40 px }
. notfound . icon { font - size : 64 px ; margin - bottom : 16 px }
. notfound h2 { font - size : 22 px ; margin - bottom : 8 px }
. notfound p { color : var ( - - text3 ) ; margin - bottom : 20 px }
/ * Recent list * /
. recent - page { flex : 1 ; padding : 24 px ; max - width : 960 px ; margin : 0 auto ; width : 100 % }
. recent - page h2 { font - size : 18 px ; margin - bottom : 16 px }
. paste - list { display : flex ; flex - direction : column ; gap : 8 px }
. paste - item { display : flex ; align - items : center ; gap : 12 px ; padding : 12 px 16 px ; background : var ( - - bg2 ) ; border : 1 px solid var ( - - border ) ; border - radius : var ( - - radius ) ; transition : border .15 s }
. paste - item : hover { border - color : var ( - - accent - dim ) }
. paste - item . pi - slug { font - family : var ( - - mono ) ; font - size : 13 px ; font - weight : 600 ; color : var ( - - accent ) ; min - width : 80 px }
. paste - item . pi - title { flex : 1 ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; color : var ( - - text2 ) ; font - size : 13 px }
. paste - item . pi - meta { font - size : 11 px ; color : var ( - - text4 ) ; display : flex ; gap : 10 px ; white - space : nowrap }
/ * API docs * /
. docs - page { flex : 1 ; padding : 24 px ; max - width : 960 px ; margin : 0 auto ; width : 100 % }
. docs - page h2 { font - size : 22 px ; margin - bottom : 8 px }
. docs - page . intro { color : var ( - - text2 ) ; margin - bottom : 24 px ; line - height : 1.6 }
. endpoint { background : var ( - - bg2 ) ; border : 1 px solid var ( - - border ) ; border - radius : var ( - - radius ) ; margin - bottom : 12 px ; overflow : hidden }
. endpoint summary { padding : 14 px 16 px ; cursor : pointer ; display : flex ; align - items : center ; gap : 10 px ; font - size : 13 px ; font - weight : 500 ; list - style : none }
. endpoint summary : : - webkit - details - marker { display : none }
. endpoint summary : : before { content : ' ▸ ' ; color : var ( - - text4 ) ; font - size : 11 px ; transition : transform .15 s }
. endpoint [ open ] summary : : before { transform : rotate ( 90 deg ) }
. endpoint . method { font - family : var ( - - mono ) ; font - size : 11 px ; font - weight : 700 ; padding : 3 px 8 px ; border - radius : 5 px ; letter - spacing : .5 px }
. method - get { background : rgba ( 34 , 197 , 94 , .12 ) ; color : var ( - - green ) }
. method - post { background : rgba ( 42 , 164 , 221 , .12 ) ; color : var ( - - accent ) }
. method - put { background : rgba ( 245 , 158 , 11 , .12 ) ; color : var ( - - orange ) }
. method - delete { background : rgba ( 239 , 68 , 68 , .12 ) ; color : var ( - - red ) }
. endpoint . path { font - family : var ( - - mono ) ; color : var ( - - text2 ) ; font - size : 13 px }
. endpoint . desc { color : var ( - - text3 ) ; font - size : 13 px ; margin - left : auto }
. endpoint . body { padding : 0 16 px 16 px ; font - size : 13 px ; color : var ( - - text2 ) ; line - height : 1.6 }
. endpoint . body h4 { font - size : 12 px ; font - weight : 600 ; color : var ( - - text ) ; margin : 12 px 0 6 px ; text - transform : uppercase ; letter - spacing : .5 px }
. endpoint . body pre { background : var ( - - bg ) ; border : 1 px solid var ( - - border ) ; border - radius : 7 px ; padding : 12 px ; overflow - x : auto ; font - family : var ( - - mono ) ; font - size : 12 px ; line - height : 1.5 ; color : var ( - - accent2 ) ; margin : 6 px 0 }
. endpoint . body code { font - family : var ( - - mono ) ; font - size : 12 px ; background : var ( - - bg3 ) ; padding : 1 px 5 px ; border - radius : 3 px }
. endpoint . body table { width : 100 % ; border - collapse : collapse ; margin : 6 px 0 }
. endpoint . body th , . endpoint . body td { text - align : left ; padding : 6 px 10 px ; border - bottom : 1 px solid var ( - - border ) ; font - size : 12 px }
. endpoint . body th { color : var ( - - text3 ) ; font - weight : 500 ; font - size : 11 px ; text - transform : uppercase ; letter - spacing : .5 px }
/ * Footer * /
. ftr { border - top : 1 px solid var ( - - border ) ; padding : 10 px 24 px ; text - align : center ; font - size : 11 px ; color : var ( - - text4 ) ; background : var ( - - bg2 ) }
. ftr code { color : var ( - - text3 ) }
/ * Toast * /
. toast { position : fixed ; bottom : 24 px ; right : 24 px ; background : var ( - - bg3 ) ; border : 1 px solid var ( - - border ) ; color : var ( - - text ) ; padding : 10 px 18 px ; border - radius : 8 px ; font - size : 13 px ; z - index : 999 ; transform : translateY ( 20 px ) ; opacity : 0 ; transition : all .2 s ; pointer - events : none }
. toast . show { transform : translateY ( 0 ) ; opacity : 1 }
/ * Responsive * /
@media ( max - width : 640 px ) {
. hdr { padding : 10 px 14 px ; flex - wrap : wrap }
. hdr nav { gap : 4 px }
. editor , . viewer { padding : 10 px }
. editor . top - bar { flex - direction : column }
. code - wrap . line - nums { padding - left : 10 px ; padding - right : 10 px }
. code - wrap . code - body { padding : 12 px 10 px }
. btn { padding : 5 px 10 px ; font - size : 12 px }
. result - card { padding : 24 px 16 px }
. docs - page , . recent - page { padding : 16 px }
. paste - item { flex - direction : column ; align - items : flex - start ; gap : 6 px }
}
2026-05-17 13:36:02 +00:00
< / style >
"""
2026-05-20 20:33:44 +00:00
HLJS_HEAD = """
< link rel = " stylesheet " href = " https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark-dimmed.min.css " >
< script src = " https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js " > < / script >
"""
TOAST_JS = """ <div class= " toast " id= " toast " ></div>
< script > function toast ( m , d = 2000 ) { const t = document . getElementById ( ' toast ' ) ; t . textContent = m ; t . classList . add ( ' show ' ) ; setTimeout ( ( ) = > t . classList . remove ( ' show ' ) , d ) } < / script > """
2026-05-17 13:36:02 +00:00
2026-05-20 20:33:44 +00:00
def page_head ( title , extra_head = " " ) :
2026-05-17 13:36:02 +00:00
return f """ <!DOCTYPE html>
< html lang = " en " > < head > < meta charset = " UTF-8 " > < meta name = " viewport " content = " width=device-width,initial-scale=1 " >
2026-05-20 20:33:44 +00:00
< title > { html_mod . escape ( title ) } < / title >
< link rel = " icon " href = " {FAVICON} " >
{ CSS } { extra_head } < / head > < body > """
def page_nav ( slug = None , extra_right = " " ) :
left = f ' <a href= " / " style= " display:flex;align-items:center;gap:10px " > { ICON_SVG } <h1>Forge Paste</h1></a> '
if slug :
left + = f ' <span class= " slug " >/ { slug } </span> '
return f """ <div class= " hdr " ><div class= " left " > { left } </div>
< nav > { extra_right }
< a href = " /recent " class = " btn btn-s " > Recent < / a >
< a href = " /api " class = " btn btn-s " > API < / a >
< a href = " / " class = " btn btn-p " > + New < / a >
< / nav > < / div > """
def page_footer ( host ) :
return f ' <div class= " ftr " >Forge Paste v { VERSION } · <code>curl -X POST -d " text " { html_mod . escape ( host ) } /api/paste</code></div> '
def page_editor ( host , fork_paste = None ) :
stats = get_stats ( )
title_val = " "
content_val = " "
lang_val = " "
if fork_paste :
title_val = html_mod . escape ( fork_paste . get ( " title " ) or " " )
content_val = html_mod . escape ( fork_paste [ " content " ] )
lang_val = fork_paste . get ( " language " ) or " "
lang_options = ' <option value= " " >Auto-detect</option> '
for l in LANGUAGES :
sel = ' selected ' if l == lang_val else ' '
lang_options + = f ' <option value= " { l } " { sel } > { l } </option> '
expiry_options = " "
for label , val in [ ( " 10 minutes " , " 10m " ) , ( " 1 hour " , " 1h " ) , ( " 1 day " , " 1d " ) , ( " 7 days " , " 7d " ) , ( " 30 days " , " 30d " ) , ( " 90 days " , " 90d " ) , ( " 1 year " , " 1y " ) , ( " Never " , " never " ) ] :
sel = ' selected ' if val == " 30d " else ' '
expiry_options + = f ' <option value= " { val } " { sel } > { label } </option> '
return f """ { page_head ( " Forge Paste " ) }
{ page_nav ( extra_right = f ' <span class= " meta " > { stats [ " active_pastes " ] : , } pastes · { format_bytes ( stats [ " total_size " ] ) } </span><span class= " sep " >·</span> ' ) }
< form class = " editor " id = " form " >
< div class = " top-bar " >
< input type = " text " name = " title " placeholder = " Title (optional) " value = " {title_val} " id = " title " maxlength = " 120 " >
< select name = " language " id = " lang " > { lang_options } < / select >
< select name = " expires " id = " expires " > { expiry_options } < / select >
< / div >
< textarea name = " content " placeholder = " Paste your code, logs, config, or text here... " autofocus id = " ta " > { content_val } < / textarea >
< div class = " bottom-bar " >
< div class = " info " >
< span id = " chars " > < / span >
< span id = " lines " > < / span >
< / div >
< div class = " opts " >
< label class = " chk " > < input type = " checkbox " id = " burn " > < span class = " box " > < / span > Burn after read < / label >
< label class = " chk " > < input type = " checkbox " id = " unlisted " > < span class = " box " > < / span > Unlisted < / label >
< button type = " submit " class = " btn btn-p " id = " btn " > 💾 Save Paste < / button >
< / div >
< / div >
2026-05-17 13:36:02 +00:00
< / form >
2026-05-20 20:33:44 +00:00
{ page_footer ( host ) }
{ TOAST_JS }
2026-05-17 13:36:02 +00:00
< script >
2026-05-20 20:33:44 +00:00
const ta = document . getElementById ( ' ta ' ) , ch = document . getElementById ( ' chars ' ) , ln = document . getElementById ( ' lines ' ) ;
function upd ( ) { {
const v = ta . value ;
ch . textContent = v . length ? v . length . toLocaleString ( ) + ' chars ' : ' ' ;
ln . textContent = v . length ? ( v . split ( ' \\ n ' ) . length ) + ' lines ' : ' ' ;
} }
ta . addEventListener ( ' input ' , upd ) ; upd ( ) ;
ta . addEventListener ( ' keydown ' , e = > { { if ( e . key == = ' Tab ' ) { { e . preventDefault ( ) ; const s = ta . selectionStart , en = ta . selectionEnd ; ta . value = ta . value . substring ( 0 , s ) + ' \\ t ' + ta . value . substring ( en ) ; ta . selectionStart = ta . selectionEnd = s + 1 ; upd ( ) } } } } ) ;
document . addEventListener ( ' keydown ' , e = > { { if ( ( e . ctrlKey | | e . metaKey ) & & e . key == = ' Enter ' ) document . getElementById ( ' form ' ) . dispatchEvent ( new Event ( ' submit ' ) ) } } ) ;
document . getElementById ( ' form ' ) . addEventListener ( ' submit ' , async e = > { {
e . preventDefault ( ) ; const btn = document . getElementById ( ' btn ' ) ;
if ( ! ta . value . trim ( ) ) return toast ( ' Nothing to paste! ' ) ;
btn . disabled = true ; btn . textContent = ' Saving... ' ;
try { {
const body = { {
content : ta . value ,
title : document . getElementById ( ' title ' ) . value | | undefined ,
language : document . getElementById ( ' lang ' ) . value | | undefined ,
expires : document . getElementById ( ' expires ' ) . value ,
burn : document . getElementById ( ' burn ' ) . checked ,
unlisted : document . getElementById ( ' unlisted ' ) . checked
} } ;
const r = await fetch ( ' /api/paste ' , { { method : ' POST ' , headers : { { ' Content-Type ' : ' application/json ' } } , body : JSON . stringify ( body ) } } ) ;
const d = await r . json ( ) ;
if ( d . key ) window . location = ' / ' + d . key + ' ?new=1&et= ' + encodeURIComponent ( d . edit_token | | ' ' ) ;
else { { toast ( d . error | | ' Error ' ) ; btn . disabled = false ; btn . textContent = ' 💾 Save Paste ' ; } }
} } catch ( err ) { { toast ( ' Network error ' ) ; btn . disabled = false ; btn . textContent = ' 💾 Save Paste ' ; } }
} } ) ;
2026-05-17 13:36:02 +00:00
< / script > < / body > < / html > """
2026-05-20 20:33:44 +00:00
def page_viewer ( paste , host , is_new = False , edit_token = None ) :
slug = paste [ " slug " ]
lang = paste . get ( " language " ) or " plaintext "
title = paste . get ( " title " ) or slug
2026-05-17 13:36:02 +00:00
ta = time_ago ( paste [ " created_at " ] )
v = paste [ " views " ]
2026-05-20 20:33:44 +00:00
size = format_bytes ( paste . get ( " size " , len ( paste [ " content " ] ) ) )
burn = paste . get ( " burn_after_read " , 0 )
burned = paste . get ( " _burned " , False )
parent = paste . get ( " parent_slug " )
2026-05-17 13:36:02 +00:00
url = f " { host } / { slug } "
2026-05-20 20:33:44 +00:00
lines = paste [ " content " ] . split ( " \n " )
lc = len ( lines )
gw = len ( str ( lc ) )
expiry_html = " "
ea = paste . get ( " expires_at " )
if ea :
rem = ea - time . time ( )
if rem > 86400 :
expiry_html = f ' <span class= " tag tag-exp " >expires in { int ( rem / / 86400 ) } d</span> '
elif rem > 3600 :
expiry_html = f ' <span class= " tag tag-exp " >expires in { int ( rem / / 3600 ) } h</span> '
elif rem > 0 :
expiry_html = ' <span class= " tag tag-exp " >expires soon</span> '
line_nums = " \n " . join ( f ' <a href= " #L { i } " id= " L { i } " > { i } </a> ' for i in range ( 1 , lc + 1 ) )
code_escaped = html_mod . escape ( paste [ " content " ] )
2026-05-17 13:36:02 +00:00
if is_new :
2026-05-20 20:33:44 +00:00
et_param = f " &et= { html_mod . escape ( edit_token ) } " if edit_token else " "
et_display = f ' <div class= " sub " >Edit token: <code> { html_mod . escape ( edit_token or " " ) } </code> — save this to edit/delete later</div> ' if edit_token else " "
return f """ { page_head ( f " { title } — Forge Paste " ) }
{ page_nav ( slug ) }
< div class = " result " > < div class = " result-card " >
< div class = " icon " > 🔥 < / div >
< h2 > Paste created ! < / h2 >
< div class = " url-box " > < a href = " / {slug} " > { html_mod . escape ( url ) } < / a > < / div >
< div class = " actions " >
< button class = " btn btn-p " onclick = " navigator.clipboard.writeText( ' { html_mod.escape(url)} ' );toast( ' URL copied! ' ) " > 📋 Copy URL < / button >
< a href = " / {slug} " class = " btn btn-s " > View < / a >
< a href = " /api/raw/ {slug} " class = " btn btn-s " > Raw < / a >
< a href = " / " class = " btn btn-s " > + New < / a >
< / div >
{ et_display }
{ " <div class= ' sub ' style= ' color:var(--red) ' >🔥 This paste will be destroyed after being read once</div> " if burn else " " }
< / div > < / div >
{ page_footer ( host ) } { TOAST_JS } < / body > < / html > """
burn_html = " "
if burned :
burn_html = ' <div class= " burn-notice " ><svg width= " 16 " height= " 16 " fill= " none " stroke= " currentColor " stroke-width= " 2 " viewBox= " 0 0 24 24 " ><path d= " M12 9v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z " /></svg>🔥 This paste has been burned — this is the only time you can view it.</div> '
elif burn :
burn_html = ' <div class= " burn-notice " ><svg width= " 16 " height= " 16 " fill= " none " stroke= " currentColor " stroke-width= " 2 " viewBox= " 0 0 24 24 " ><path d= " M12 9v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z " /></svg>🔥 Burn after read — this paste will self-destruct after being viewed</div> '
meta_parts = [ ta , f " { v } view { ' s ' if v != 1 else ' ' } " , size , f " { lc } lines " ]
meta_str = " · " . join ( meta_parts )
parent_html = f ' · forked from <a href= " / { parent } " > { parent } </a> ' if parent else " "
right_btns = f """
< span class = " meta " > { meta_str } { parent_html } < / span > < span class = " sep " > · < / span >
< button class = " btn btn-s " onclick = " navigator.clipboard.writeText(document.getElementById( ' raw ' ).textContent);toast( ' Copied! ' ) " > 📋 Copy < / button >
< a href = " /api/raw/ {slug} " class = " btn btn-s " > Raw < / a >
< a href = " /api/download/ {slug} " class = " btn btn-s " > ⬇ Download < / a >
< a href = " / {slug} /fork " class = " btn btn-s " > 🍴 Fork < / a > """
return f """ { page_head ( f " { title } — Forge Paste " , HLJS_HEAD ) }
{ page_nav ( slug , extra_right = right_btns ) }
< div class = " viewer " >
< div class = " toolbar " >
< div class = " tags " >
< span class = " tag tag-lang " > { html_mod . escape ( lang ) } < / span >
{ expiry_html }
{ " <span class= ' tag tag-burn ' >🔥 burn</span> " if burn and not burned else " " }
< / div >
{ f ' <h3 style= " font-size:15px;font-weight:600 " > { html_mod . escape ( paste . get ( " title " ) or " " ) } </h3> ' if paste . get ( " title " ) else " " }
< / div >
{ burn_html }
< div class = " code-wrap " >
< div class = " lines " >
< div class = " line-nums " > { line_nums } < / div >
< div class = " code-body " > < pre > < code class = " language- { html_mod.escape(lang)} " id = " hl " > { code_escaped } < / code > < / pre > < / div >
< / div >
< / div >
< / div >
< pre id = " raw " style = " display:none " > { code_escaped } < / pre >
{ page_footer ( host ) } { TOAST_JS }
< script >
hljs . highlightElement ( document . getElementById ( ' hl ' ) ) ;
/ / Line highlight from hash
function highlightLine ( ) { {
document . querySelectorAll ( ' .line-nums a.active ' ) . forEach ( a = > a . classList . remove ( ' active ' ) ) ;
const h = location . hash ; if ( h & & h . startsWith ( ' #L ' ) ) { {
const el = document . getElementById ( h . slice ( 1 ) ) ; if ( el ) { { el . classList . add ( ' active ' ) ; el . scrollIntoView ( { { block : ' center ' } } ) } }
} }
} }
window . addEventListener ( ' hashchange ' , highlightLine ) ; highlightLine ( ) ;
< / script > < / body > < / html > """
2026-05-17 13:36:02 +00:00
def page_notfound ( ) :
2026-05-20 20:33:44 +00:00
return f """ { page_head ( " Not Found — Forge Paste " ) }
{ page_nav ( ) }
< div class = " notfound " > < div >
< div class = " icon " > 🔍 < / div >
< h2 > Paste not found < / h2 >
< p > This paste may have expired , been burned , or never existed . < / p >
< a href = " / " class = " btn btn-p " > + Create New Paste < / a >
< / div > < / div > < / body > < / html > """
def page_recent ( host , pastes ) :
items = " "
for p in pastes :
t = html_mod . escape ( p . get ( " title " ) or " " )
slug = p [ " slug " ]
lang = p . get ( " language " ) or " ? "
views = p . get ( " views " , 0 )
ta_str = time_ago ( p [ " created_at " ] )
sz = format_bytes ( p . get ( " size " , 0 ) )
burn_tag = ' <span class= " tag tag-burn " style= " font-size:10px " >🔥</span> ' if p . get ( " burn_after_read " ) else " "
items + = f """ <a href= " / { slug } " class= " paste-item " >
< span class = " pi-slug " > { slug } < / span >
< span class = " pi-title " > { t or ' <em style= " color:var(--text4) " >untitled</em> ' } { burn_tag } < / span >
< span class = " pi-meta " > < span class = " tag tag-lang " style = " font-size:10px " > { html_mod . escape ( lang ) } < / span > < span > { sz } < / span > < span > { views } views < / span > < span > { ta_str } < / span > < / span >
< / a > """
return f """ { page_head ( " Recent — Forge Paste " ) }
{ page_nav ( ) }
< div class = " recent-page " >
< h2 > 📋 Recent Pastes < / h2 >
< div class = " paste-list " > { items if items else ' <p style= " color:var(--text3);padding:40px;text-align:center " >No pastes yet. <a href= " / " >Create one!</a></p> ' } < / div >
< / div > { page_footer ( host ) } < / body > < / html > """
def page_api_docs ( host ) :
h = html_mod . escape ( host )
return f """ { page_head ( " API — Forge Paste " ) }
{ page_nav ( ) }
< div class = " docs-page " >
< h2 > 🔧 API Documentation < / h2 >
< p class = " intro " > Forge Paste provides a full REST API with hastebin compatibility . All endpoints accept and return JSON . Base URL : < code > { h } < / code > < / p >
< details class = " endpoint " open >
< summary > < span class = " method method-post " > POST < / span > < span class = " path " > / api / paste < / span > < span class = " desc " > Create a new paste < / span > < / summary >
< div class = " body " >
< h4 > Request ( JSON ) < / h4 >
< table > < tr > < th > Field < / th > < th > Type < / th > < th > Required < / th > < th > Description < / th > < / tr >
< tr > < td > < code > content < / code > < / td > < td > string < / td > < td > ✅ < / td > < td > Paste content ( max 2 MB ) < / td > < / tr >
< tr > < td > < code > title < / code > < / td > < td > string < / td > < td > < / td > < td > Optional title ( max 120 chars ) < / td > < / tr >
< tr > < td > < code > language < / code > < / td > < td > string < / td > < td > < / td > < td > Syntax language ( auto - detected if omitted ) < / td > < / tr >
< tr > < td > < code > expires < / code > < / td > < td > string < / td > < td > < / td > < td > Expiry : < code > 10 m < / code > < code > 1 h < / code > < code > 1 d < / code > < code > 7 d < / code > < code > 30 d < / code > < code > 90 d < / code > < code > 1 y < / code > < code > never < / code > < / td > < / tr >
< tr > < td > < code > burn < / code > < / td > < td > bool < / td > < td > < / td > < td > Destroy after first read < / td > < / tr >
< tr > < td > < code > unlisted < / code > < / td > < td > bool < / td > < td > < / td > < td > Hide from recent list < / td > < / tr >
< / table >
< h4 > Alternative : Raw Body < / h4 >
< p > Send raw text as the body with any content type other than < code > application / json < / code > : < / p >
< pre > curl - X POST - d " Hello World " { h } / api / paste < / pre >
< h4 > Response < / h4 >
< pre > { {
" key " : " a1b2c3d4 " ,
" url " : " {h} /a1b2c3d4 " ,
" raw_url " : " {h} /api/raw/a1b2c3d4 " ,
" edit_token " : " xyz... " ,
" expires_at " : 1716300000.0
} } < / pre >
< h4 > Examples < / h4 >
< pre > # Simple paste
curl - X POST - d " your text here " { h } / api / paste
# JSON with options
curl - X POST - H " Content-Type: application/json " \\
- d ' {{ " content " : " print(42) " , " language " : " python " , " expires " : " 1h " , " burn " :true}} ' \\
{ h } / api / paste
# Pipe a file
cat script . py | curl - X POST - d @ - { h } / api / paste
# With title
curl - X POST - H " Content-Type: application/json " \\
- d ' {{ " content " : " ... " , " title " : " My Config " }} ' { h } / api / paste < / pre >
< / div > < / details >
< details class = " endpoint " >
< summary > < span class = " method method-get " > GET < / span > < span class = " path " > / api / paste / & lt ; slug & gt ; < / span > < span class = " desc " > Get paste metadata & content < / span > < / summary >
< div class = " body " >
< h4 > Response < / h4 >
< pre > { {
" key " : " a1b2c3d4 " ,
" title " : " My Paste " ,
" content " : " ... " ,
" language " : " python " ,
" created_at " : 1716200000.0 ,
" views " : 42 ,
" size " : 1234 ,
" expires_at " : 1716300000.0
} } < / pre >
< pre > curl { h } / api / paste / a1b2c3d4 < / pre >
< / div > < / details >
< details class = " endpoint " >
< summary > < span class = " method method-get " > GET < / span > < span class = " path " > / api / raw / & lt ; slug & gt ; < / span > < span class = " desc " > Get raw text content < / span > < / summary >
< div class = " body " >
< p > Returns the paste content as < code > text / plain < / code > . < / p >
< pre > curl { h } / api / raw / a1b2c3d4 < / pre >
< / div > < / details >
< details class = " endpoint " >
< summary > < span class = " method method-get " > GET < / span > < span class = " path " > / api / download / & lt ; slug & gt ; < / span > < span class = " desc " > Download as file < / span > < / summary >
< div class = " body " >
< p > Returns the paste with < code > Content - Disposition : attachment < / code > header . Optional < code > ? filename = name . ext < / code > query param . < / p >
< pre > curl - OJ { h } / api / download / a1b2c3d4 < / pre >
< / div > < / details >
< details class = " endpoint " >
< summary > < span class = " method method-put " > PUT < / span > < span class = " path " > / api / paste / & lt ; slug & gt ; < / span > < span class = " desc " > Update a paste < / span > < / summary >
< div class = " body " >
< h4 > Request < / h4 >
< table > < tr > < th > Field < / th > < th > Type < / th > < th > Required < / th > < th > Description < / th > < / tr >
< tr > < td > < code > edit_token < / code > < / td > < td > string < / td > < td > ✅ < / td > < td > Token from create response < / td > < / tr >
< tr > < td > < code > content < / code > < / td > < td > string < / td > < td > ✅ < / td > < td > New content < / td > < / tr >
< tr > < td > < code > title < / code > < / td > < td > string < / td > < td > < / td > < td > New title < / td > < / tr >
< tr > < td > < code > language < / code > < / td > < td > string < / td > < td > < / td > < td > New language < / td > < / tr >
< / table >
< pre > curl - X PUT - H " Content-Type: application/json " \\
- d ' {{ " edit_token " : " xyz... " , " content " : " updated " }} ' \\
{ h } / api / paste / a1b2c3d4 < / pre >
< / div > < / details >
< details class = " endpoint " >
< summary > < span class = " method method-delete " > DELETE < / span > < span class = " path " > / api / paste / & lt ; slug & gt ; < / span > < span class = " desc " > Delete a paste < / span > < / summary >
< div class = " body " >
< h4 > Request < / h4 >
< pre > curl - X DELETE - H " Content-Type: application/json " \\
- d ' {{ " edit_token " : " xyz... " }} ' \\
{ h } / api / paste / a1b2c3d4 < / pre >
< / div > < / details >
< details class = " endpoint " >
< summary > < span class = " method method-get " > GET < / span > < span class = " path " > / api / pastes / recent < / span > < span class = " desc " > List recent public pastes < / span > < / summary >
< div class = " body " >
< p > Query : < code > ? limit = 20 & amp ; offset = 0 < / code > < / p >
< pre > curl { h } / api / pastes / recent ? limit = 10 < / pre >
< / div > < / details >
< details class = " endpoint " >
< summary > < span class = " method method-get " > GET < / span > < span class = " path " > / api / stats < / span > < span class = " desc " > Server statistics < / span > < / summary >
< div class = " body " >
< pre > curl { h } / api / stats < / pre >
< pre > { {
" total_pastes " : 420 ,
" active_pastes " : 380 ,
" total_views " : 12500 ,
" total_size " : 2048000 ,
" version " : " {VERSION} "
} } < / pre >
< / div > < / details >
2026-05-17 13:36:02 +00:00
2026-05-20 20:33:44 +00:00
< details class = " endpoint " >
< summary > < span class = " method method-get " > GET < / span > < span class = " path " > / api / languages < / span > < span class = " desc " > List supported languages < / span > < / summary >
< div class = " body " >
< pre > curl { h } / api / languages < / pre >
< / div > < / details >
2026-05-17 13:36:02 +00:00
2026-05-20 20:33:44 +00:00
< details class = " endpoint " >
< summary > < span class = " method method-get " > GET < / span > < span class = " path " > / api / health < / span > < span class = " desc " > Health check < / span > < / summary >
< div class = " body " >
< pre > curl { h } / api / health < / pre >
< pre > { { " status " : " ok " , " version " : " {VERSION} " } } < / pre >
< / div > < / details >
< details class = " endpoint " >
< summary > < span class = " method method-post " > POST < / span > < span class = " path " > / api / documents < / span > < span class = " desc " > Hastebin - compatible create < / span > < / summary >
< div class = " body " >
< p > Send raw text body . Returns < code > { { " key " : " ... " } } < / code > < / p >
< pre > curl - X POST - d " hello " { h } / api / documents < / pre >
< / div > < / details >
< details class = " endpoint " >
< summary > < span class = " method method-get " > GET < / span > < span class = " path " > / api / documents / & lt ; slug & gt ; < / span > < span class = " desc " > Hastebin - compatible get < / span > < / summary >
< div class = " body " >
< pre > curl { h } / api / documents / a1b2c3d4 < / pre >
< pre > { { " key " : " a1b2c3d4 " , " data " : " ... " } } < / pre >
< / div > < / details >
< h3 style = " margin-top:32px;font-size:16px " > 🖥 ️ CLI Aliases < / h3 >
< p class = " intro " > Add these to your < code > . bashrc < / code > or < code > . zshrc < / code > : < / p >
< pre style = " background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:16px;font-family:var(--mono);font-size:12px;line-height:1.6;color:var(--accent2);overflow-x:auto " > # Paste from clipboard
alias fpaste = ' xclip -selection clipboard -o | curl -s -X POST -d @- {h} /api/paste | jq -r .url '
# Paste a file
fp ( ) { { curl - s - X POST - d @ " $1 " { h } / api / paste | jq - r . url ; } }
# Paste from stdin
alias fp - = ' curl -s -X POST -d @- {h} /api/paste | jq -r .url '
# Get a paste
fpget ( ) { { curl - s { h } / api / raw / " $1 " ; } }
< / pre >
< / div > { page_footer ( host ) } < / body > < / html > """
# ── HTTP Handler ────────────────────────────────────────────
2026-05-17 13:36:02 +00:00
class PasteHandler ( BaseHTTPRequestHandler ) :
2026-05-20 20:33:44 +00:00
server_version = f " ForgePaste/ { VERSION } "
2026-05-17 13:36:02 +00:00
def log_message ( self , format , * args ) :
2026-05-20 20:33:44 +00:00
pass # Quiet
def _json ( self , code , data ) :
body = json . dumps ( data ) . encode ( )
2026-05-17 13:36:02 +00:00
self . send_response ( code )
2026-05-20 20:33:44 +00:00
self . send_header ( " Content-Type " , " application/json; charset=utf-8 " )
self . _cors ( )
self . send_header ( " Content-Length " , str ( len ( body ) ) )
2026-05-17 13:36:02 +00:00
self . end_headers ( )
2026-05-20 20:33:44 +00:00
self . wfile . write ( body )
def _html ( self , code , content ) :
body = content . encode ( " utf-8 " )
self . send_response ( code )
self . send_header ( " Content-Type " , " text/html; charset=utf-8 " )
self . _cors ( )
self . send_header ( " Content-Length " , str ( len ( body ) ) )
self . end_headers ( )
self . wfile . write ( body )
def _text ( self , code , content , filename = None ) :
body = content . encode ( " utf-8 " ) if isinstance ( content , str ) else content
self . send_response ( code )
if filename :
self . send_header ( " Content-Type " , " application/octet-stream " )
self . send_header ( " Content-Disposition " , f ' attachment; filename= " { filename } " ' )
else :
self . send_header ( " Content-Type " , " text/plain; charset=utf-8 " )
self . _cors ( )
self . send_header ( " Content-Length " , str ( len ( body ) ) )
self . end_headers ( )
self . wfile . write ( body )
def _cors ( self ) :
self . send_header ( " Access-Control-Allow-Origin " , " * " )
self . send_header ( " Access-Control-Allow-Methods " , " GET, POST, PUT, DELETE, OPTIONS " )
self . send_header ( " Access-Control-Allow-Headers " , " Content-Type, Authorization " )
2026-05-17 13:36:02 +00:00
def _host ( self ) :
proto = self . headers . get ( " X-Forwarded-Proto " , " http " )
host = self . headers . get ( " X-Forwarded-Host " ) or self . headers . get ( " Host " , " localhost " )
return f " { proto } :// { host } "
2026-05-20 20:33:44 +00:00
def _ip ( self ) :
return self . headers . get ( " X-Real-IP " ) or self . client_address [ 0 ]
def _read_body ( self ) :
try :
length = int ( self . headers . get ( " Content-Length " , 0 ) )
except ( ValueError , TypeError ) :
return None , " Invalid Content-Length "
if length > MAX_SIZE :
return None , f " Content too large (max { format_bytes ( MAX_SIZE ) } ) "
return self . rfile . read ( length ) , None
2026-05-17 13:36:02 +00:00
def do_OPTIONS ( self ) :
2026-05-20 20:33:44 +00:00
self . send_response ( 204 )
self . _cors ( )
self . end_headers ( )
2026-05-17 13:36:02 +00:00
def do_GET ( self ) :
parsed = urlparse ( self . path )
2026-05-20 20:33:44 +00:00
path = parsed . path . rstrip ( " / " ) or " / "
query = parse_qs ( parsed . query )
2026-05-17 13:36:02 +00:00
host = self . _host ( )
2026-05-20 20:33:44 +00:00
# Static pages
if path == " / " :
return self . _html ( 200 , page_editor ( host ) )
if path == " /recent " :
pastes = get_recent ( limit = 50 )
return self . _html ( 200 , page_recent ( host , pastes ) )
if path == " /api " or path == " /api/docs " :
return self . _html ( 200 , page_api_docs ( host ) )
# API: health
if path == " /api/health " :
return self . _json ( 200 , { " status " : " ok " , " version " : VERSION } )
# API: stats
if path == " /api/stats " :
return self . _json ( 200 , get_stats ( ) )
# API: languages
if path == " /api/languages " :
return self . _json ( 200 , { " languages " : LANGUAGES } )
# API: recent
if path == " /api/pastes/recent " :
limit = min ( int ( query . get ( " limit " , [ 20 ] ) [ 0 ] ) , 100 )
offset = max ( int ( query . get ( " offset " , [ 0 ] ) [ 0 ] ) , 0 )
return self . _json ( 200 , { " pastes " : get_recent ( limit , offset ) } )
# API: raw
2026-05-17 13:36:02 +00:00
if path . startswith ( " /api/raw/ " ) :
slug = path [ 9 : ]
paste = get_paste ( slug )
if not paste :
2026-05-20 20:33:44 +00:00
return self . _text ( 404 , " Not found " )
return self . _text ( 200 , paste [ " content " ] )
# API: download
if path . startswith ( " /api/download/ " ) :
slug = path [ 14 : ]
paste = get_paste ( slug )
if not paste :
return self . _text ( 404 , " Not found " )
lang = paste . get ( " language " ) or " txt "
ext_map = { " python " : " py " , " javascript " : " js " , " typescript " : " ts " , " bash " : " sh " , " ruby " : " rb " ,
" rust " : " rs " , " csharp " : " cs " , " plaintext " : " txt " , " markdown " : " md " , " yaml " : " yml " }
ext = ext_map . get ( lang , lang )
fname = query . get ( " filename " , [ f " { slug } . { ext } " ] ) [ 0 ]
return self . _text ( 200 , paste [ " content " ] , filename = fname )
# API: paste detail
if path . startswith ( " /api/paste/ " ) :
slug = path [ 11 : ]
paste = get_paste ( slug )
if not paste :
return self . _json ( 404 , { " error " : " not found " } )
return self . _json ( 200 , {
" key " : paste [ " slug " ] , " title " : paste . get ( " title " ) ,
" content " : paste [ " content " ] , " language " : paste . get ( " language " ) ,
" created_at " : paste [ " created_at " ] , " updated_at " : paste . get ( " updated_at " ) ,
" views " : paste [ " views " ] , " size " : paste . get ( " size " ) ,
" expires_at " : paste . get ( " expires_at " ) ,
" burn_after_read " : bool ( paste . get ( " burn_after_read " ) ) ,
} )
# API: hastebin compat
2026-05-17 13:36:02 +00:00
if path . startswith ( " /api/documents/ " ) :
slug = path [ 15 : ]
paste = get_paste ( slug )
if not paste :
2026-05-20 20:33:44 +00:00
return self . _json ( 404 , { " error " : " not found " } )
return self . _json ( 200 , { " key " : paste [ " slug " ] , " data " : paste [ " content " ] } )
# Web: fork
if path . endswith ( " /fork " ) :
slug = path [ 1 : - 5 ]
paste = get_paste ( slug , increment_view = False )
if not paste :
return self . _html ( 404 , page_notfound ( ) )
paste [ " parent_slug " ] = slug
return self . _html ( 200 , page_editor ( host , fork_paste = paste ) )
# Web: view paste
slug = path [ 1 : ]
if slug and re . match ( r ' ^[a-z0-9]+$ ' , slug ) :
2026-05-17 13:36:02 +00:00
paste = get_paste ( slug )
if paste :
2026-05-20 20:33:44 +00:00
is_new = " new " in query
et = query . get ( " et " , [ None ] ) [ 0 ]
return self . _html ( 200 , page_viewer ( paste , host , is_new , edit_token = et ) )
return self . _html ( 404 , page_notfound ( ) )
self . _html ( 404 , page_notfound ( ) )
2026-05-17 13:36:02 +00:00
def do_POST ( self ) :
2026-05-20 20:33:44 +00:00
body , err = self . _read_body ( )
if err :
return self . _json ( 400 , { " error " : err } )
2026-05-17 13:36:02 +00:00
parsed = urlparse ( self . path )
path = parsed . path . rstrip ( " / " )
host = self . _host ( )
2026-05-20 20:33:44 +00:00
ip = self . _ip ( )
# Hastebin compat
2026-05-17 13:36:02 +00:00
if path == " /api/documents " :
2026-05-20 20:33:44 +00:00
text = body . decode ( " utf-8 " , errors = " replace " )
if not text . strip ( ) :
return self . _json ( 400 , { " error " : " Empty content " } )
slug , _ = create_paste ( text , ip = ip )
return self . _json ( 200 , { " key " : slug } )
# Main create
if path == " /api/paste " :
ct = self . headers . get ( " Content-Type " , " " )
if " application/json " in ct :
try :
data = json . loads ( body )
except json . JSONDecodeError :
return self . _json ( 400 , { " error " : " Invalid JSON " } )
content = data . get ( " content " , " " )
title = data . get ( " title " )
language = data . get ( " language " )
expires_key = data . get ( " expires " , " 30d " )
burn = data . get ( " burn " , False )
unlisted = data . get ( " unlisted " , False )
parent = data . get ( " parent_slug " )
else :
content = body . decode ( " utf-8 " , errors = " replace " )
title = None
language = None
expires_key = " 30d "
burn = False
unlisted = False
parent = None
if not content or not content . strip ( ) :
return self . _json ( 400 , { " error " : " Empty content " } )
if len ( content . encode ( ) ) > MAX_SIZE :
return self . _json ( 413 , { " error " : f " Content too large (max { format_bytes ( MAX_SIZE ) } ) " } )
expires_in = EXPIRY_PRESETS . get ( expires_key , EXPIRY_PRESETS [ " 30d " ] )
slug , edit_token = create_paste (
content , title = title , language = language , ip = ip ,
expires_in = expires_in , burn = burn ,
is_public = not unlisted , parent_slug = parent
)
result = {
" key " : slug , " url " : f " { host } / { slug } " ,
" raw_url " : f " { host } /api/raw/ { slug } " ,
" edit_token " : edit_token ,
}
if expires_in > 0 :
result [ " expires_at " ] = time . time ( ) + expires_in
return self . _json ( 200 , result )
self . _json ( 404 , { " error " : " Not found " } )
def do_PUT ( self ) :
body , err = self . _read_body ( )
if err :
return self . _json ( 400 , { " error " : err } )
parsed = urlparse ( self . path )
path = parsed . path . rstrip ( " / " )
if path . startswith ( " /api/paste/ " ) :
slug = path [ 11 : ]
try :
data = json . loads ( body )
except json . JSONDecodeError :
return self . _json ( 400 , { " error " : " Invalid JSON " } )
2026-05-17 13:36:02 +00:00
2026-05-20 20:33:44 +00:00
token = data . get ( " edit_token " , " " )
if not token :
return self . _json ( 401 , { " error " : " edit_token required " } )
paste = get_paste ( slug , increment_view = False )
if not paste :
return self . _json ( 404 , { " error " : " not found " } )
if paste . get ( " edit_token " ) != token :
return self . _json ( 403 , { " error " : " Invalid edit_token " } )
content = data . get ( " content " )
if not content or not content . strip ( ) :
return self . _json ( 400 , { " error " : " Empty content " } )
update_paste ( slug , content , title = data . get ( " title " ) , language = data . get ( " language " ) )
return self . _json ( 200 , { " key " : slug , " updated " : True } )
self . _json ( 404 , { " error " : " Not found " } )
def do_DELETE ( self ) :
body , err = self . _read_body ( )
if err :
return self . _json ( 400 , { " error " : err } )
parsed = urlparse ( self . path )
path = parsed . path . rstrip ( " / " )
if path . startswith ( " /api/paste/ " ) :
slug = path [ 11 : ]
try :
data = json . loads ( body ) if body else { }
except json . JSONDecodeError :
return self . _json ( 400 , { " error " : " Invalid JSON " } )
token = data . get ( " edit_token " , " " )
if not token :
return self . _json ( 401 , { " error " : " edit_token required " } )
paste = get_paste ( slug , increment_view = False )
if not paste :
return self . _json ( 404 , { " error " : " not found " } )
if paste . get ( " edit_token " ) != token :
return self . _json ( 403 , { " error " : " Invalid edit_token " } )
delete_paste ( slug )
return self . _json ( 200 , { " key " : slug , " deleted " : True } )
self . _json ( 404 , { " error " : " Not found " } )
# ── Threaded server ─────────────────────────────────────────
from socketserver import ThreadingMixIn
class ThreadedHTTPServer ( ThreadingMixIn , HTTPServer ) :
daemon_threads = True
allow_reuse_address = True
2026-05-17 13:36:02 +00:00
def main ( ) :
2026-05-19 11:17:01 +00:00
global DB_PATH , HOST , PORT , MAX_AGE_DAYS
2026-05-20 20:33:44 +00:00
2026-05-17 13:36:02 +00:00
parser = argparse . ArgumentParser ( description = " Forge Paste Server " )
2026-05-20 20:33:44 +00:00
parser . add_argument ( " --port " , type = int , default = PORT )
parser . add_argument ( " --host " , default = HOST )
parser . add_argument ( " --db " , default = DB_PATH )
parser . add_argument ( " --max-age " , type = int , default = MAX_AGE_DAYS )
2026-05-17 13:36:02 +00:00
args = parser . parse_args ( )
2026-05-20 20:33:44 +00:00
2026-05-17 13:36:02 +00:00
DB_PATH = args . db
HOST = args . host
PORT = args . port
2026-05-19 11:17:01 +00:00
MAX_AGE_DAYS = args . max_age
2026-05-20 20:33:44 +00:00
2026-05-17 13:36:02 +00:00
init_db ( )
2026-05-20 20:33:44 +00:00
server = ThreadedHTTPServer ( ( HOST , PORT ) , PasteHandler )
print ( f " 🔥 Forge Paste v { VERSION } running on http:// { HOST } : { PORT } " )
2026-05-17 13:36:02 +00:00
print ( f " 📦 Database: { DB_PATH } " )
2026-05-20 20:33:44 +00:00
print ( f " ⏱ Max age: { MAX_AGE_DAYS } days (0=never) " )
2026-05-17 13:36:02 +00:00
try :
server . serve_forever ( )
except KeyboardInterrupt :
print ( " \n Shutting down... " )
server . server_close ( )
if __name__ == " __main__ " :
main ( )