scripts/upload.py
scripts/upload.pyBrowse 5 files
1,016 tokens
3,817 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32"""3Upload an .excalidraw file to excalidraw.com and print a shareable URL.4 5No account required. The diagram is encrypted client-side (AES-GCM) before6upload -- the encryption key is embedded in the URL fragment, so the server7never sees plaintext.8 9Requirements:10 pip install cryptography11 12Usage:13 python upload.py <path-to-file.excalidraw>14 15Example:16 python upload.py ~/diagrams/architecture.excalidraw17 # prints: https://excalidraw.com/#json=abc123,encryptionKeyHere18"""19 20import json21import os22import struct23import sys24import zlib25import base6426import urllib.request27 28try:29 from cryptography.hazmat.primitives.ciphers.aead import AESGCM30except ImportError:31 print("Error: 'cryptography' package is required for upload.")32 print("Install it with: pip install cryptography")33 sys.exit(1)34 35# Excalidraw public upload endpoint (no auth needed)36UPLOAD_URL = "https://json.excalidraw.com/api/v2/post/"37 38 39def concat_buffers(*buffers: bytes) -> bytes:40 """41 Build the Excalidraw v2 concat-buffers binary format.42 43 Layout: [version=1 (4B big-endian)] then for each buffer:44 [length (4B big-endian)] [data bytes]45 """46 parts = [struct.pack(">I", 1)] # version = 147 for buf in buffers:48 parts.append(struct.pack(">I", len(buf)))49 parts.append(buf)50 return b"".join(parts)51 52 53def upload(excalidraw_json: str) -> str:54 """55 Encrypt and upload Excalidraw JSON to excalidraw.com.56 57 Args:58 excalidraw_json: The full .excalidraw file content as a string.59 60 Returns:61 Shareable URL string.62 """63 # 1. Inner payload: concat_buffers(file_metadata, data)64 file_metadata = json.dumps({}).encode("utf-8")65 data_bytes = excalidraw_json.encode("utf-8")66 inner_payload = concat_buffers(file_metadata, data_bytes)67 68 # 2. Compress with zlib69 compressed = zlib.compress(inner_payload)70 71 # 3. AES-GCM 128-bit encrypt72 raw_key = os.urandom(16) # 128-bit key73 iv = os.urandom(12) # 12-byte nonce74 aesgcm = AESGCM(raw_key)75 encrypted = aesgcm.encrypt(iv, compressed, None)76 77 # 4. Encoding metadata78 encoding_meta = json.dumps({79 "version": 2,80 "compression": "pako@1",81 "encryption": "AES-GCM",82 }).encode("utf-8")83 84 # 5. Outer payload: concat_buffers(encoding_meta, iv, encrypted)85 payload = concat_buffers(encoding_meta, iv, encrypted)86 87 # 6. Upload88 req = urllib.request.Request(UPLOAD_URL, data=payload, method="POST")89 with urllib.request.urlopen(req, timeout=30) as resp:90 if resp.status != 200:91 raise RuntimeError(f"Upload failed with HTTP {resp.status}")92 result = json.loads(resp.read().decode("utf-8"))93 94 file_id = result.get("id")95 if not file_id:96 raise RuntimeError(f"Upload returned no file ID. Response: {result}")97 98 # 7. Key as base64url (JWK 'k' format, no padding)99 key_b64 = base64.urlsafe_b64encode(raw_key).rstrip(b"=").decode("ascii")100 101 return f"https://excalidraw.com/#json={file_id},{key_b64}"102 103 104def main():105 if len(sys.argv) < 2:106 print("Usage: python upload.py <path-to-file.excalidraw>")107 sys.exit(1)108 109 file_path = sys.argv[1]110 111 if not os.path.isfile(file_path):112 print(f"Error: File not found: {file_path}")113 sys.exit(1)114 115 with open(file_path, "r", encoding="utf-8") as f:116 content = f.read()117 118 # Basic validation: should be valid JSON with an "elements" key119 try:120 doc = json.loads(content)121 except json.JSONDecodeError as e:122 print(f"Error: File is not valid JSON: {e}")123 sys.exit(1)124 125 if "elements" not in doc:126 print("Warning: File does not contain an 'elements' key. Uploading anyway.")127 128 url = upload(content)129 print(url)130 131 132if __name__ == "__main__":133 main()134