159 lines
5.1 KiB
Python
Executable File
159 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# /// script
|
|
# dependencies = [
|
|
# "boto3>=1.34.0",
|
|
# ]
|
|
# ///
|
|
"""
|
|
Publish files and static HTML documents to public S3 hosting at hosting.haumdaucher.de.
|
|
Organizes artifacts under /{category}/{YYYYMMDD}-{slug}.{ext}.
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import mimetypes
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
import boto3
|
|
from botocore.client import Config
|
|
|
|
# Configuration defaults
|
|
DEFAULT_ENDPOINT = os.environ.get("MINIO_ENDPOINT", "http://minio.minio.svc.cluster.local:9000")
|
|
DEFAULT_ACCESS_KEY = os.environ.get("MINIO_ACCESS_KEY", "openclaw")
|
|
DEFAULT_SECRET_KEY = os.environ.get("MINIO_SECRET_KEY", "5uu23t69xC2Oigf8Bky0KTlI")
|
|
DEFAULT_BUCKET = os.environ.get("MINIO_BUCKET", "hosting")
|
|
DEFAULT_BASE_URL = os.environ.get("HOSTING_BASE_URL", "https://hosting.haumdaucher.de").rstrip("/")
|
|
|
|
MIME_OVERRIDES = {
|
|
".html": "text/html; charset=utf-8",
|
|
".htm": "text/html; charset=utf-8",
|
|
".md": "text/markdown; charset=utf-8",
|
|
".json": "application/json; charset=utf-8",
|
|
".pdf": "application/pdf",
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".webp": "image/webp",
|
|
".svg": "image/svg+xml",
|
|
".txt": "text/plain; charset=utf-8",
|
|
}
|
|
|
|
def get_s3_client():
|
|
if not DEFAULT_SECRET_KEY:
|
|
raise ValueError("MINIO_SECRET_KEY environment variable is not configured.")
|
|
|
|
return boto3.client(
|
|
"s3",
|
|
endpoint_url=DEFAULT_ENDPOINT,
|
|
aws_access_key_id=DEFAULT_ACCESS_KEY,
|
|
aws_secret_access_key=DEFAULT_SECRET_KEY,
|
|
config=Config(signature_version="s3v4"),
|
|
region_name="us-east-1",
|
|
)
|
|
|
|
def slugify(text: str) -> str:
|
|
"""Converts raw string to URL-safe slug."""
|
|
text = text.strip().lower()
|
|
text = re.sub(r"[^\w\s-]", "", text)
|
|
text = re.sub(r"[\s_-]+", "-", text)
|
|
return text.strip("-") or "document"
|
|
|
|
def detect_mime(ext: str) -> str:
|
|
ext_lower = ext.lower()
|
|
if ext_lower in MIME_OVERRIDES:
|
|
return MIME_OVERRIDES[ext_lower]
|
|
guessed, _ = mimetypes.guess_type(f"file{ext_lower}")
|
|
return guessed or "application/octet-stream"
|
|
|
|
def upload_artifact(file_path: str = None, content: str = None, category: str = "research", slug: str = None, ext: str = None) -> dict:
|
|
s3 = get_s3_client()
|
|
now_str = datetime.datetime.now().strftime("%Y%m%d")
|
|
|
|
# Determine file data and extension
|
|
if file_path:
|
|
p = Path(file_path)
|
|
if not p.exists():
|
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
with open(p, "rb") as f:
|
|
data = f.read()
|
|
chosen_ext = (ext or p.suffix).lstrip(".")
|
|
chosen_slug = slug or slugify(p.stem)
|
|
elif content is not None:
|
|
data = content.encode("utf-8")
|
|
chosen_ext = (ext or "html").lstrip(".")
|
|
chosen_slug = slug or f"doc-{datetime.datetime.now().strftime('%H%M%S')}"
|
|
else:
|
|
raise ValueError("Either --file or --content must be specified.")
|
|
|
|
chosen_slug = slugify(chosen_slug)
|
|
category_slug = slugify(category)
|
|
|
|
# Construct standard object key: {category}/{YYYYMMDD}-{slug}.{ext}
|
|
filename = f"{now_str}-{chosen_slug}.{chosen_ext}"
|
|
object_key = f"{category_slug}/{filename}"
|
|
|
|
content_type = detect_mime(f".{chosen_ext}")
|
|
|
|
s3.put_object(
|
|
Bucket=DEFAULT_BUCKET,
|
|
Key=object_key,
|
|
Body=data,
|
|
ContentType=content_type,
|
|
)
|
|
|
|
public_url = f"{DEFAULT_BASE_URL}/{object_key}"
|
|
|
|
return {
|
|
"url": public_url,
|
|
"key": object_key,
|
|
"category": category_slug,
|
|
"filename": filename,
|
|
"content_type": content_type,
|
|
"bytes": len(data),
|
|
}
|
|
|
|
def delete_artifact(object_key: str):
|
|
s3 = get_s3_client()
|
|
s3.delete_object(Bucket=DEFAULT_BUCKET, Key=object_key)
|
|
print(f"Deleted: {object_key}")
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Publish files to hosting.haumdaucher.de.")
|
|
parser.add_argument("--file", "-f", help="Local file path to upload")
|
|
parser.add_argument("--content", "-c", help="Inline text or HTML string to publish")
|
|
parser.add_argument("--category", default="research", help="Category folder (e.g. research, recipes, images, docs)")
|
|
parser.add_argument("--slug", "-s", help="Readable slug identifier for the document")
|
|
parser.add_argument("--ext", help="File extension override (e.g. html, pdf, png)")
|
|
parser.add_argument("--delete", help="Delete an object key from the hosting bucket")
|
|
parser.add_argument("--url-only", action="store_true", help="Print only the public URL to stdout")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.delete:
|
|
delete_artifact(args.delete)
|
|
return
|
|
|
|
if not args.file and args.content is None:
|
|
parser.error("You must provide either --file or --content.")
|
|
|
|
result = upload_artifact(
|
|
file_path=args.file,
|
|
content=args.content,
|
|
category=args.category,
|
|
slug=args.slug,
|
|
ext=args.ext,
|
|
)
|
|
|
|
if args.url_only:
|
|
print(result["url"])
|
|
else:
|
|
print(f"URL: {result['url']}")
|
|
print(f"KEY: {result['key']}")
|
|
print(f"TYPE: {result['content_type']}")
|
|
print(f"SIZE: {result['bytes']} bytes")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|