diff --git a/.gitignore b/.gitignore
index 634f0f0..8686580 100644
--- a/.gitignore
+++ b/.gitignore
@@ -161,3 +161,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
+
+pastes/*
+!pastes/keep
diff --git a/Dockerfile b/Dockerfile
index 70300bc..7866111 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.9-slim-buster
+FROM python:3.9-slim-bullseye
WORKDIR /app
diff --git a/README.md b/README.md
index 170555d..be37285 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,11 @@ sce run z
```
- [ ] ensure the server is running locally at `http://localhost:8000`
+if youre running cleezy on sce.sjsu.edu, make an .env file like
+```
+CLEEZY_PASTE_API_KEY=NOTHING_REALLY
+```
+
## APIs
### To add URL
send HTTP POST request to http://localhost:8000/create_url with body
@@ -37,6 +42,24 @@ Open http://localhost:8000/list in the browser
- send HTTP POST request to http://localhost:8000/delete/myurl
- verify the url was deleted by opening http://localhost:8000/list in the browser
+### To create a paste
+if you didnt make an env file like above, no need to pass in api key
+```sh
+curl -X POST "http://localhost:8000/paste/create" \
+ -H "Content-Type: application/json" \
+ -H "X-API-Key: your_secret_key_here" \
+ -d '{"title": "My First Paste", "text": "hello2"}'
+
+# example response is
+# {"status":"success","id":"6556e","url":"/paste/6556e"}
+```
+
+### To view a paste
+```sh
+# put the paste id after the `/paste/` in the url, like below
+curl http://localhost:8000/paste/6556e
+```
+
## SQLite Migrations
If you have an existing database and want to add a column, see below
```sh
@@ -50,20 +73,3 @@ apt install -y sqlite3
ALTER TABLE urls
ADD COLUMN expires_at DATETIME DEFAULT NULL;
```
-## QR Embeds
-### tl;dr THEY DON'T WORK ON DISCORD
-
-here's the discord embed debugger that tries to scrape embed stuff from the url it is provided. 'internal error' means discord wasn't able to do this
-
-
-
-here's some logs from cleezy-nginx after the embed debugger was run and a twitter post with the QR url was sent
-```
-172.25.0.14 - - [01/Sep/2025:19:03:43 +0000] "GET /qr/EMBEDTEST1 HTTP/1.0" 404 185 "-" "Twitterbot/1.0"
-172.25.0.14 - - [01/Sep/2025:19:03:43 +0000] "GET /qr/EMBEDTEST1 HTTP/1.0" 404 185 "-" "Twitterbot/1.0"
-```
-as you can see, only twitter was able to connect to the site and scrape, display embed stuff
-
-
-
-ATP, the source of the issue is not very apparent
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 0eca4bc..224caeb 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -12,6 +12,7 @@ services:
- --qr-code-base-url=http://localhost:8000/find
- --port=8000
- --qr-code-center-image-path=/app/assets/SCE_logo.png
+ - --paste-directory=/app/pastes
- -vvv
ports:
- 8000:8000
@@ -20,6 +21,7 @@ services:
- ./server.py:/app/server.py
- ./modules:/app/modules
- ./assets:/app/assets
+ - ./pastes:/app/pastes
environment:
- WATCHFILES_FORCE_POLLING=true
diff --git a/docker-compose.yml b/docker-compose.yml
index d347454..b1d868e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,12 +11,16 @@ services:
- --qr-code-cache-state-file=/tmp/cleezy-state
- --qr-code-base-url=https://sce.sjsu.edu/s
- --qr-code-center-image-path=/app/assets/SCE_logo.png
+ - --paste-directory=/app/pastes
- -vvv
volumes:
- cleezy_data:/tmp/
- ./server.py:/app/server.py
- ./modules:/app/modules
- ./assets:/app/assets
+ - ./pastes:/app/pastes
+ env_file:
+ - .env
volumes:
cleezy_data:
diff --git a/modules/args.py b/modules/args.py
index b2cf099..8ad36ab 100644
--- a/modules/args.py
+++ b/modules/args.py
@@ -68,4 +68,9 @@ def get_args():
default="America/Los_Angeles",
help="the timezone that url expiration checks will use. defaults to America/Los_Angeles"
)
+ parser.add_argument(
+ "--paste-directory",
+ default="/app/pastes",
+ help="the timezone that url expiration checks will use. defaults to America/Los_Angeles"
+ )
return parser.parse_args()
diff --git a/modules/constants.py b/modules/constants.py
index f74225d..fee9a5c 100644
--- a/modules/constants.py
+++ b/modules/constants.py
@@ -8,12 +8,19 @@
Return to homepage
"""
+REQUEST_TOO_LARGE_HTML = """
+Request too large.
+
+The bytes sent was size "{request_size}" which exceeds max size of {max_size}.
+"""
+
class HttpResponse(enum.Enum):
OK = (200, "success")
BAD_REQUEST = (400, "No URL was found in your request
")
NOT_FOUND = (404, NOT_FOUND_HTML)
CONFLICT = (409, "Alias already exists
")
+ REQUEST_TOO_LARGE = (413, REQUEST_TOO_LARGE_HTML)
INVALID_ARGUMENT_EXCEPTION = (422, "Alias is invalid
")
INTERNAL_SERVER_ERROR = (500, "Internal server error
")
def __init__(self, code, content):
@@ -25,6 +32,7 @@ def __init__(self, code, content):
400: HttpResponse.BAD_REQUEST,
404: HttpResponse.NOT_FOUND,
409: HttpResponse.CONFLICT,
+ 413: HttpResponse.REQUEST_TOO_LARGE,
422: HttpResponse.INVALID_ARGUMENT_EXCEPTION,
500: HttpResponse.INTERNAL_SERVER_ERROR,
}
diff --git a/modules/sqlite_helpers.py b/modules/sqlite_helpers.py
index 62a3b6f..45d0629 100644
--- a/modules/sqlite_helpers.py
+++ b/modules/sqlite_helpers.py
@@ -38,6 +38,7 @@ def maybe_create_table(sqlite_file: str) -> bool:
cursor.execute(create_pastes_table_query)
cursor.execute(create_table_query)
cursor.execute(create_index_query)
+
db.commit()
return True
except Exception:
@@ -195,3 +196,22 @@ def increment_used_column(sqlite_file, alias: str, count=1):
finally:
cursor.close()
db.close()
+
+def insert_paste(sqlite_file: str, paste_id: str, title: str):
+ db = sqlite3.connect(sqlite_file)
+ cursor = db.cursor()
+
+ try:
+ sql = "INSERT INTO pastes(id, title) VALUES (?, ?)"
+ val = (paste_id, title)
+ cursor.execute(sql, val)
+ db.commit()
+ return True
+ except sqlite3.IntegrityError:
+ return False
+ except Exception:
+ logger.exception("Inserting paste had an error")
+ return False
+ finally:
+ cursor.close()
+ db.close()
diff --git a/pastes/keep b/pastes/keep
new file mode 100644
index 0000000..e69de29
diff --git a/server.py b/server.py
index f56a5b1..bd72314 100644
--- a/server.py
+++ b/server.py
@@ -1,6 +1,6 @@
from typing import Optional
from fastapi import FastAPI, Request, HTTPException, Response
-from fastapi.responses import RedirectResponse, HTMLResponse, FileResponse
+from fastapi.responses import RedirectResponse, HTMLResponse, FileResponse, PlainTextResponse
from fastapi.middleware.cors import CORSMiddleware
import logging
import time
@@ -18,9 +18,19 @@
from modules.cache import Cache
from modules.qr_code import QRCode
+from pathlib import Path
+import os
+
+CLEEZY_PASTE_API_KEY = os.getenv("CLEEZY_PASTE_API_KEY")
+
+MAX_PASTE_SIZE_BYTES = 10 * 1024 * 1024
app = FastAPI()
args = get_args()
+
+PASTES_DIR = Path(args.paste_directory)
+PASTES_DIR.mkdir(exist_ok=True)
+
alias_queue = Queue()
app.add_middleware(
@@ -156,6 +166,56 @@ async def delete_url(alias: str):
else:
raise HTTPException(status_code=HttpResponse.NOT_FOUND.code)
+@app.post("/paste/create")
+async def create_paste(request: Request):
+ api_key = request.headers.get("x-api-key")
+
+ if CLEEZY_PASTE_API_KEY is None:
+ logging.warning("CLEEZY_PASTE_API_KEY isn't set, skipping api key check")
+ elif api_key != CLEEZY_PASTE_API_KEY:
+ raise HTTPException(status_code=401, detail=f"Invalid API Key '{api_key}'")
+
+ try:
+ payload = await request.json()
+ except Exception:
+ logging.exception("/paste/create couldnt parse json")
+ raise HTTPException(
+ status_code=HttpResponse.BAD_REQUEST.code,
+ detail="Invalid JSON payload"
+ )
+ text_bytes = payload.get("text", "").encode("utf-8")
+ if len(text_bytes) > MAX_PASTE_SIZE_BYTES:
+ raise HTTPException(
+ status_code=HttpResponse.REQUEST_TOO_LARGE,
+ detail="Paste content exceeds the maximum allowed size of 10MB."
+ )
+
+ paste_id = generate_alias(len(payload.get('text')))
+
+ success = sqlite_helpers.insert_paste(DATABASE_FILE, paste_id, payload.get('title', 'Untitled Paste'))
+ if not success:
+ raise HTTPException(
+ status_code=HttpResponse.INTERNAL_SERVER_ERROR,
+ detail="Failed to save paste metadata."
+ )
+
+ paste_path = PASTES_DIR / str(paste_id)
+ paste_path.write_bytes(text_bytes)
+
+ return {
+ "status": "success",
+ "id": paste_id,
+ "url": f"/paste/{paste_id}"
+ }
+
+
+@app.get("/paste/{paste_id}")
+async def view_paste(paste_id: str):
+ paste_path = PASTES_DIR / paste_id
+ if not paste_path.exists():
+ raise HTTPException(status_code=HttpResponse.NOT_FOUND.code)
+ return PlainTextResponse(paste_path.read_text(encoding="utf-8"))
+
@app.get("/qr/{alias}")
async def qr(alias: str):
logging.debug(f"/qr code generation called with alias: {alias}")
@@ -178,6 +238,11 @@ async def qr(alias: str):
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
+ if exc.status_code not in http_code_to_enum:
+ return HTMLResponse(
+ status_code=exc.status_code,
+ content=exc.detail,
+ )
status_code_enum = http_code_to_enum[exc.status_code]
content = status_code_enum.content
if status_code_enum == HttpResponse.NOT_FOUND:
@@ -187,6 +252,18 @@ async def http_exception_handler(request, exc):
requested_url=str(original_url),
base_url=str(base_url)
)
+ if status_code_enum == HttpResponse.REQUEST_TOO_LARGE:
+ request_size = "Unknown"
+ try:
+ body = await request.json()
+ if isinstance(body, dict) and "text" in body:
+ request_size = len(body["text"].encode("utf-8"))
+ except Exception:
+ pass
+ content = content.format(
+ request_size=request_size,
+ max_size=MAX_PASTE_SIZE_BYTES,
+ )
return HTMLResponse(
content=content, status_code=status_code_enum.code
)