unknown Claude Opus 5.5 commited on
Commit
cdcade6
·
1 Parent(s): e7bb050

Open in Overleaf: respect Overleaf's request size limit

Browse files

Overleaf answered a posted project with "Something went wrong ... there
was a problem with your request": HTTP 413. Measured limit: a 1.98 MB
request is accepted, 2.13 MB is refused, i.e. a zip of about 1.4 MB once
it is base64 in a form. The cut-off had been a guessed 6 MB.

- The button now measures the real request and posts the project only
when it stays under OVERLEAF_POST_MB (1.8): a zip up to about 1.2 MB.
- A larger project is handed over by its public address on the Space
(snip_uri=https://...), which Overleaf downloads itself.
- With neither possible the page says so and explains the manual upload.
- Test that every posted project fits the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Files changed (2) hide show
  1. formatter_tab.py +15 -9
  2. tests/test_tools_tabs.py +15 -3
formatter_tab.py CHANGED
@@ -8,7 +8,8 @@ a compliance report. Equations, algorithms and complex tables are carried over u
8
  Overleaf: the LaTeX project is a zip with main.tex at its top level, refs.bib and figures/, using classes that
9
  Overleaf's TeX Live has (IEEEtran, elsarticle, llncs, sn-jnl ...). "Open in Overleaf" posts that zip to Overleaf
10
  itself (overleaf.com/devs: snip_uri as a base64 data URL), so it works on a private Space and on a local run; only
11
- a very large project is handed over by its address on the Space (SPACE_HOST) instead.
 
12
  """
13
  import html
14
  import os
@@ -54,8 +55,11 @@ def public_url(path):
54
 
55
 
56
  OVERLEAF = "https://www.overleaf.com/docs" # Overleaf's "Open in Overleaf" API (overleaf.com/devs)
57
- # A project up to this size is sent to Overleaf inside the button itself; a larger one by its address on the Space.
58
- OVERLEAF_EMBED_MB = float(os.getenv("OVERLEAF_EMBED_MB", "6"))
 
 
 
59
 
60
 
61
  def overleaf_html(zip_path):
@@ -70,10 +74,11 @@ def overleaf_html(zip_path):
70
  '<code>refs.bib</code> and the figures in <code>figures/</code>; compile with pdfLaTeX.</div>')
71
  if not zip_path or not os.path.exists(zip_path):
72
  return ""
73
- size_mb = os.path.getsize(zip_path) / (1024 * 1024)
74
- if size_mb <= OVERLEAF_EMBED_MB:
75
- import base64
76
- data = "data:application/zip;base64," + base64.b64encode(Path(zip_path).read_bytes()).decode("ascii")
 
77
  return (f'<form action="{OVERLEAF}" method="post" target="_blank" style="margin:0">'
78
  f'<input type="hidden" name="snip_uri" value="{data}">'
79
  '<input type="hidden" name="engine" value="pdflatex">'
@@ -81,8 +86,9 @@ def overleaf_html(zip_path):
81
  '<button type="submit" class="cs-dl" style="border:0;cursor:pointer;font:inherit">'
82
  '🍃 Open in Overleaf</button></form>' + manual)
83
  url = public_url(zip_path)
84
- if not url:
85
- return manual
 
86
  link = f"{OVERLEAF}?engine=pdflatex&main_document=main.tex&snip_uri=" + quote(url, safe="")
87
  return (f'<a class="cs-dl" target="_blank" rel="noopener" href="{html.escape(link, quote=True)}">'
88
  f'🍃 Open in Overleaf</a>' + manual)
 
8
  Overleaf: the LaTeX project is a zip with main.tex at its top level, refs.bib and figures/, using classes that
9
  Overleaf's TeX Live has (IEEEtran, elsarticle, llncs, sn-jnl ...). "Open in Overleaf" posts that zip to Overleaf
10
  itself (overleaf.com/devs: snip_uri as a base64 data URL), so it works on a private Space and on a local run; only
11
+ a project too large for Overleaf to accept in one request (a zip above about 1.2 MB) is handed over by its address on
12
+ the Space (SPACE_HOST) instead.
13
  """
14
  import html
15
  import os
 
55
 
56
 
57
  OVERLEAF = "https://www.overleaf.com/docs" # Overleaf's "Open in Overleaf" API (overleaf.com/devs)
58
+ # Overleaf answers a posted request above about 2 MB with "Something went wrong ... problem with your request"
59
+ # (HTTP 413; measured: 1.98 MB accepted, 2.13 MB refused). The project is base64 inside a form, which makes it
60
+ # about 1.42 times its size on the wire, so only a zip whose REQUEST stays under OVERLEAF_POST_MB is posted
61
+ # (a zip up to roughly 1.2 MB); a larger one is handed to Overleaf by its address on the Space.
62
+ OVERLEAF_POST_MB = float(os.getenv("OVERLEAF_POST_MB", "1.8"))
63
 
64
 
65
  def overleaf_html(zip_path):
 
74
  '<code>refs.bib</code> and the figures in <code>figures/</code>; compile with pdfLaTeX.</div>')
75
  if not zip_path or not os.path.exists(zip_path):
76
  return ""
77
+ import base64
78
+ from urllib.parse import urlencode
79
+ data = "data:application/zip;base64," + base64.b64encode(Path(zip_path).read_bytes()).decode("ascii")
80
+ request_mb = len(urlencode({"snip_uri": data, "engine": "pdflatex", "main_document": "main.tex"})) / 1e6
81
+ if request_mb <= OVERLEAF_POST_MB:
82
  return (f'<form action="{OVERLEAF}" method="post" target="_blank" style="margin:0">'
83
  f'<input type="hidden" name="snip_uri" value="{data}">'
84
  '<input type="hidden" name="engine" value="pdflatex">'
 
86
  '<button type="submit" class="cs-dl" style="border:0;cursor:pointer;font:inherit">'
87
  '🍃 Open in Overleaf</button></form>' + manual)
88
  url = public_url(zip_path)
89
+ if not url: # too large to post and no public address: by hand
90
+ return ('<div class="cs-gauge-cap" style="text-align:left">This project is too large to send to Overleaf '
91
+ 'directly.</div>' + manual)
92
  link = f"{OVERLEAF}?engine=pdflatex&main_document=main.tex&snip_uri=" + quote(url, safe="")
93
  return (f'<a class="cs-dl" target="_blank" rel="noopener" href="{html.escape(link, quote=True)}">'
94
  f'🍃 Open in Overleaf</a>' + manual)
tests/test_tools_tabs.py CHANGED
@@ -81,8 +81,8 @@ def test_overleaf_button_posts_the_project_itself(tmp_path):
81
 
82
 
83
  def test_a_large_project_goes_by_its_address_or_by_hand(tmp_path, monkeypatch):
84
- p = _zip(tmp_path, kb=64)
85
- monkeypatch.setattr(FMT, "OVERLEAF_EMBED_MB", 0.01)
86
  monkeypatch.setenv("SPACE_HOST", "rasheed-ai-writing-suite.hf.space")
87
  h = FMT.overleaf_html(p)
88
  assert "<form" not in h and "snip_uri=https%3A%2F%2Frasheed-ai-writing-suite.hf.space%2Fgradio_api%2Ffile%3D" in h
@@ -90,10 +90,22 @@ def test_a_large_project_goes_by_its_address_or_by_hand(tmp_path, monkeypatch):
90
  monkeypatch.delenv("SPACE_HOST")
91
  monkeypatch.delenv("PUBLIC_HOST", raising=False)
92
  h = FMT.overleaf_html(p)
93
- assert "Open in Overleaf" not in h and "Upload Project" in h
94
  assert FMT.overleaf_html(None) == ""
95
 
96
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  # ---------------------------------------------------------------- finder
98
  def test_finder_files_and_data_are_in_place():
99
  assert FND.AVAILABLE
 
81
 
82
 
83
  def test_a_large_project_goes_by_its_address_or_by_hand(tmp_path, monkeypatch):
84
+ p = _zip(tmp_path, kb=2000) # a 2 MB project: Overleaf refuses it as a posted form
85
+ assert FMT.OVERLEAF_POST_MB < 2.0
86
  monkeypatch.setenv("SPACE_HOST", "rasheed-ai-writing-suite.hf.space")
87
  h = FMT.overleaf_html(p)
88
  assert "<form" not in h and "snip_uri=https%3A%2F%2Frasheed-ai-writing-suite.hf.space%2Fgradio_api%2Ffile%3D" in h
 
90
  monkeypatch.delenv("SPACE_HOST")
91
  monkeypatch.delenv("PUBLIC_HOST", raising=False)
92
  h = FMT.overleaf_html(p)
93
+ assert "Open in Overleaf" not in h and "too large to send" in h and "Upload Project" in h
94
  assert FMT.overleaf_html(None) == ""
95
 
96
 
97
+ def test_a_posted_project_stays_under_overleafs_request_limit(tmp_path):
98
+ from urllib.parse import urlencode
99
+ for kb in (200, 900, 1200): # every project that is posted fits Overleaf's 2 MB
100
+ h = FMT.overleaf_html(_zip(tmp_path, kb=kb))
101
+ if "<form" in h:
102
+ data = h.split('name="snip_uri" value="', 1)[1].split('"', 1)[0]
103
+ body = len(urlencode({"snip_uri": data, "engine": "pdflatex", "main_document": "main.tex"}))
104
+ assert body <= 1.9e6, kb
105
+ assert "<form" in FMT.overleaf_html(_zip(tmp_path, kb=200))
106
+ assert "<form" not in FMT.overleaf_html(_zip(tmp_path, kb=1600))
107
+
108
+
109
  # ---------------------------------------------------------------- finder
110
  def test_finder_files_and_data_are_in_place():
111
  assert FND.AVAILABLE