starfriend commited on
Commit
ce675eb
·
verified ·
1 Parent(s): b15889e

Upload 3 files

Browse files
metadata_search/app.py CHANGED
@@ -273,12 +273,9 @@ def paper_pdf_file(filename: str):
273
  )
274
 
275
 
276
- @app.get("/figures/<path:paper_key>/<path:filename>")
277
  def paper_figure_file(paper_key: str, filename: str):
278
- safe_paper_key = Path(paper_key).name
279
- safe_name = Path(filename).name
280
-
281
- target = resolve_figure_asset(safe_paper_key, safe_name)
282
  if target is None:
283
  return jsonify({"error": "Figure file not found."}), 404
284
 
 
273
  )
274
 
275
 
276
+ @app.get("/figures/<path:paper_key>/<filename>")
277
  def paper_figure_file(paper_key: str, filename: str):
278
+ target = resolve_figure_asset(paper_key, filename)
 
 
 
279
  if target is None:
280
  return jsonify({"error": "Figure file not found."}), 404
281
 
metadata_search/config.py CHANGED
@@ -55,7 +55,7 @@ METADATA_SEARCH_PORT = int(
55
  PAPER_FIGURES_DIR = Path(
56
  os.getenv(
57
  "PAPER_FIGURES_DIR",
58
- str(PROJECT_ROOT / "results_metadata (2025-2026)"),
59
  )
60
  )
61
 
 
55
  PAPER_FIGURES_DIR = Path(
56
  os.getenv(
57
  "PAPER_FIGURES_DIR",
58
+ str(PROJECT_ROOT / "Media"),
59
  )
60
  )
61
 
metadata_search/paper_figures.py CHANGED
@@ -6,7 +6,7 @@ import json
6
  import logging
7
  import os
8
  import unicodedata
9
- from pathlib import Path
10
  from urllib.parse import quote
11
 
12
  from metadata_search.config import PAPER_FIGURES_DIR
@@ -23,7 +23,7 @@ HF_DATASET_REPO = os.getenv(
23
  "WATERSCOPE_DATASET_REPO",
24
  "EnvironmentalAI/WaterScopeAI-Data",
25
  ).strip()
26
- HF_FIGURES_PREFIX = os.getenv("WATERSCOPE_FIGURES_PREFIX", "Figures").strip() or "Figures"
27
  HF_FIGURES_CACHE = Path(
28
  os.getenv("WATERSCOPE_FIGURES_CACHE", "/tmp/waterscope-figure-cache")
29
  ).expanduser().resolve()
@@ -66,10 +66,37 @@ def _hf_download(repo_file: str) -> Path | None:
66
  return path if path.is_file() else None
67
 
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  def resolve_figure_asset(folder: str, filename: str) -> Path | None:
70
  """Resolve one figure asset locally, then fetch only that asset if needed."""
71
- safe_folder = Path(folder).name
72
- safe_name = Path(filename).name
 
 
73
 
74
  root = figures_dir()
75
  if root is not None:
@@ -133,10 +160,13 @@ def _load_index() -> tuple[dict[str, str], dict[str, dict]]:
133
  # 1) Existing local behavior.
134
  root = figures_dir()
135
  if root is not None:
136
- for entry in sorted(root.iterdir()):
137
- if not entry.is_dir():
138
- continue
139
- manifest_path = entry / "manifest.json"
 
 
 
140
  if not manifest_path.is_file():
141
  continue
142
  try:
@@ -145,34 +175,36 @@ def _load_index() -> tuple[dict[str, str], dict[str, dict]]:
145
  logger.warning("Skipping figure manifest %s: %s", manifest_path, exc)
146
  continue
147
 
148
- folder = entry.name
149
  manifests[folder] = manifest
150
- _register_stem(stem_index, folder, folder)
151
 
152
  source_pdf = (manifest.get("source_pdf") or "").strip()
153
  if source_pdf:
154
- _register_stem(stem_index, Path(source_pdf).name, folder)
155
 
156
  # 2) Cloud fallback. List repository paths once, then download only
157
  # manifest.json files. Image assets remain on-demand.
158
  if not manifests and HF_FIGURES_ON_DEMAND:
159
  try:
160
- repo_files = _hf_api().list_repo_files(
161
- repo_id=HF_DATASET_REPO,
162
- repo_type="dataset",
163
- )
164
  suffix = "/manifest.json"
165
  prefix = f"{HF_FIGURES_PREFIX}/"
166
- manifest_files = [
167
- path for path in repo_files
168
- if path.startswith(prefix) and path.endswith(suffix)
169
- ]
 
 
 
 
 
 
 
170
 
171
  for repo_file in manifest_files:
172
  relative = repo_file[len(prefix):]
173
  folder = relative[:-len(suffix)]
174
- # The current WaterScope dataset uses one immediate paper folder.
175
- if not folder or "/" in folder:
176
  continue
177
 
178
  manifest_path = _hf_download(repo_file)
@@ -185,11 +217,11 @@ def _load_index() -> tuple[dict[str, str], dict[str, dict]]:
185
  continue
186
 
187
  manifests[folder] = manifest
188
- _register_stem(stem_index, folder, folder)
189
 
190
  source_pdf = (manifest.get("source_pdf") or "").strip()
191
  if source_pdf:
192
- _register_stem(stem_index, Path(source_pdf).name, folder)
193
 
194
  logger.info(
195
  "Indexed figures for %s papers from HF Dataset %s",
@@ -219,11 +251,11 @@ def _lookup_folder(*, txt_file: str = "", pdf_file: str = "", paper_key: str = "
219
  return paper_key
220
  target = normalize_stem(paper_key)
221
  for folder in manifests:
222
- if normalize_stem(folder) == target:
223
  return folder
224
 
225
  for candidate in (txt_file, pdf_file):
226
- stem = normalize_stem(candidate)
227
  if not stem:
228
  continue
229
  if stem in stem_index:
@@ -262,7 +294,7 @@ def resolve_figure_folder(
262
 
263
 
264
  def figure_image_url(folder: str, filename: str) -> str:
265
- return f"/figures/{quote(folder, safe='')}/{quote(filename, safe='')}"
266
 
267
 
268
  def figures_payload_for_folder(folder: str) -> dict | None:
@@ -293,7 +325,7 @@ def figures_payload_for_folder(folder: str) -> dict | None:
293
  source_pdf = (manifest.get("source_pdf") or "").strip()
294
  return {
295
  "paper_key": folder,
296
- "source_pdf": Path(source_pdf).name if source_pdf else folder,
297
  "item_count": len(items),
298
  "items": items,
299
  }
 
6
  import logging
7
  import os
8
  import unicodedata
9
+ from pathlib import Path, PurePosixPath, PureWindowsPath
10
  from urllib.parse import quote
11
 
12
  from metadata_search.config import PAPER_FIGURES_DIR
 
23
  "WATERSCOPE_DATASET_REPO",
24
  "EnvironmentalAI/WaterScopeAI-Data",
25
  ).strip()
26
+ HF_FIGURES_PREFIX = os.getenv("WATERSCOPE_FIGURES_PREFIX", "Media").strip() or "Media"
27
  HF_FIGURES_CACHE = Path(
28
  os.getenv("WATERSCOPE_FIGURES_CACHE", "/tmp/waterscope-figure-cache")
29
  ).expanduser().resolve()
 
66
  return path if path.is_file() else None
67
 
68
 
69
+ def _media_folder(folder: str) -> str | None:
70
+ """Preserve relative year/study paths without allowing path traversal."""
71
+ if not folder or "\\" in folder or ":" in folder:
72
+ return None
73
+ path = PurePosixPath(folder)
74
+ if path.is_absolute() or any(part in {"", ".", ".."} for part in folder.split("/")):
75
+ return None
76
+ return path.as_posix()
77
+
78
+
79
+ def _batch_folders(path: Path) -> list[str]:
80
+ """The overall manifest supplies study folders; yearly reports are not scanned."""
81
+ batch = json.loads(path.read_text(encoding="utf-8-sig"))
82
+ if not isinstance(batch.get("papers"), list):
83
+ raise ValueError(f"Missing papers list in media batch manifest: {path}")
84
+ folders = []
85
+ for record in batch["papers"]:
86
+ folder = _media_folder(str(record.get("output_directory") or ""))
87
+ if folder is None:
88
+ raise ValueError("Invalid media output_directory in batch manifest")
89
+ if folder not in folders:
90
+ folders.append(folder)
91
+ return folders
92
+
93
+
94
  def resolve_figure_asset(folder: str, filename: str) -> Path | None:
95
  """Resolve one figure asset locally, then fetch only that asset if needed."""
96
+ safe_folder = _media_folder(folder)
97
+ if safe_folder is None or not filename or "/" in filename or "\\" in filename or filename in {".", ".."}:
98
+ return None
99
+ safe_name = filename
100
 
101
  root = figures_dir()
102
  if root is not None:
 
160
  # 1) Existing local behavior.
161
  root = figures_dir()
162
  if root is not None:
163
+ batch_path = root / "batch_manifest.json"
164
+ folders = (_batch_folders(batch_path) if batch_path.is_file() else
165
+ [path.parent.relative_to(root).as_posix()
166
+ for path in sorted(root.rglob("manifest.json"))
167
+ if path.parent != root])
168
+ for folder in folders:
169
+ manifest_path = root / folder / "manifest.json"
170
  if not manifest_path.is_file():
171
  continue
172
  try:
 
175
  logger.warning("Skipping figure manifest %s: %s", manifest_path, exc)
176
  continue
177
 
 
178
  manifests[folder] = manifest
179
+ _register_stem(stem_index, PurePosixPath(folder).name, folder)
180
 
181
  source_pdf = (manifest.get("source_pdf") or "").strip()
182
  if source_pdf:
183
+ _register_stem(stem_index, PureWindowsPath(source_pdf).name, folder)
184
 
185
  # 2) Cloud fallback. List repository paths once, then download only
186
  # manifest.json files. Image assets remain on-demand.
187
  if not manifests and HF_FIGURES_ON_DEMAND:
188
  try:
 
 
 
 
189
  suffix = "/manifest.json"
190
  prefix = f"{HF_FIGURES_PREFIX}/"
191
+ batch_path = _hf_download(f"{prefix}batch_manifest.json")
192
+ if batch_path is not None:
193
+ manifest_files = [f"{prefix}{folder}{suffix}"
194
+ for folder in _batch_folders(batch_path)]
195
+ else:
196
+ # Support data without an overall manifest; only study manifests.
197
+ repo_files = _hf_api().list_repo_files(
198
+ repo_id=HF_DATASET_REPO, repo_type="dataset",
199
+ )
200
+ manifest_files = [path for path in repo_files
201
+ if path.startswith(prefix) and path.endswith(suffix)]
202
 
203
  for repo_file in manifest_files:
204
  relative = repo_file[len(prefix):]
205
  folder = relative[:-len(suffix)]
206
+ # Keep the year component when resolving study manifests.
207
+ if _media_folder(folder) is None:
208
  continue
209
 
210
  manifest_path = _hf_download(repo_file)
 
217
  continue
218
 
219
  manifests[folder] = manifest
220
+ _register_stem(stem_index, PurePosixPath(folder).name, folder)
221
 
222
  source_pdf = (manifest.get("source_pdf") or "").strip()
223
  if source_pdf:
224
+ _register_stem(stem_index, PureWindowsPath(source_pdf).name, folder)
225
 
226
  logger.info(
227
  "Indexed figures for %s papers from HF Dataset %s",
 
251
  return paper_key
252
  target = normalize_stem(paper_key)
253
  for folder in manifests:
254
+ if normalize_stem(folder) == target or normalize_stem(PurePosixPath(folder).name) == target:
255
  return folder
256
 
257
  for candidate in (txt_file, pdf_file):
258
+ stem = normalize_stem(PureWindowsPath(candidate).name)
259
  if not stem:
260
  continue
261
  if stem in stem_index:
 
294
 
295
 
296
  def figure_image_url(folder: str, filename: str) -> str:
297
+ return f"/figures/{quote(folder, safe='/')}/{quote(filename, safe='')}"
298
 
299
 
300
  def figures_payload_for_folder(folder: str) -> dict | None:
 
325
  source_pdf = (manifest.get("source_pdf") or "").strip()
326
  return {
327
  "paper_key": folder,
328
+ "source_pdf": PureWindowsPath(source_pdf).name if source_pdf else folder,
329
  "item_count": len(items),
330
  "items": items,
331
  }