Skip to content

Commit 13febbe

Browse files
committed
include post method for metadata mod
1 parent dc92fde commit 13febbe

1 file changed

Lines changed: 176 additions & 8 deletions

File tree

app/routers/dscheck_test.py

Lines changed: 176 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@
88
99
SECURITY: Test endpoint only. Restrict access in production.
1010
"""
11-
from fastapi import APIRouter
11+
1212
from typing import Dict, Any
1313
from datetime import datetime
14+
from pathlib import Path
15+
from fastapi import APIRouter
16+
from pydantic import BaseModel, field_validator
17+
1418

1519

1620
# Import RDA/GDEX libraries
1721
try:
1822
from rda_python_common.pg_dbi import PgDBI
23+
from rda_python_common.pg_log import PgLOG
1924
RDA_AVAILABLE = True
2025
except ImportError as e:
2126
raise RuntimeError(
@@ -26,21 +31,57 @@
2631

2732
router = APIRouter(prefix="/dscheck", tags=["dscheck_testing"])
2833

34+
35+
class MetadataRequest(BaseModel):
36+
"""Request model for metadata modification."""
37+
data_file_relpath: Path = Path('Web-services/test.nc')
38+
attr_name: str
39+
attr_value: str
40+
41+
@field_validator('data_file_relpath')
42+
@classmethod
43+
def validate_relative_path(cls, v: Path) -> Path:
44+
"""Ensure path is relative and doesn't escape base directory."""
45+
if v.is_absolute():
46+
raise ValueError(f"Path must be relative, got absolute path: {v}")
47+
48+
# Prevent path traversal attacks (../)
49+
if ".." in str(v):
50+
raise ValueError(f"Path traversal not allowed: {v}")
51+
52+
return v
53+
54+
2955
@router.get("/getinfo1")
3056
async def get_dscheck_info() -> Dict[str, Any]:
3157
"""
3258
Retrieve dscheck information for specialist 'chiaweih'.
3359
34-
**SECURITY**: This is a test endpoint only. Hardcoded values, no parameters.
60+
This is a test endpoint with hardcoded query parameters.
61+
62+
Returns
63+
-------
64+
dict
65+
Response dictionary containing:
3566
36-
Query:
37-
PgDBI.pgget("dscheck", "*", "specialist = 'chiaweih'", logact|PgLOG.EXITLG)
67+
- success (bool): Whether the query succeeded
68+
- message (str): Human-readable status message
69+
- timestamp (str): ISO format timestamp
70+
- specialist (str): The specialist name queried
71+
- record (dict): The first dscheck record matching the query, or None
72+
- error (str, optional): Error message if query failed
3873
39-
Returns:
40-
Single dscheck record with all fields or error message
74+
Notes
75+
-----
76+
**SECURITY**: This is a test endpoint only. Uses hardcoded query:
77+
- Table: dscheck
78+
- Condition: specialist = 'chiaweih'
79+
- Fields: * (all)
4180
42-
Example:
43-
GET /dscheck/getinfo1 (full path after router prefix)
81+
Examples
82+
--------
83+
>>> curl https://api_url/dscheck/getinfo1
84+
{"success": true, "message": "Successfully retrieved dscheck record...", ...}
4485
"""
4586
try:
4687
# Hardcoded query as specified
@@ -88,6 +129,133 @@ async def get_dscheck_info() -> Dict[str, Any]:
88129
}
89130

90131

132+
133+
@router.post("/metadata")
134+
async def submit_metadata_modify(
135+
request: MetadataRequest
136+
) -> Dict[str, Any]:
137+
"""
138+
Submit metadata modification for dscheck records.
139+
140+
Parameters
141+
----------
142+
request : MetadataRequest
143+
Request object containing metadata modification parameters.
144+
145+
Attributes:
146+
data_file_relpath : Path
147+
Relative path to the data file. Must be relative, not absolute.
148+
Default: 'Web-services/test.nc'
149+
attr_name : str
150+
Name of the attribute to modify.
151+
Default: 'gdex_dsid'
152+
attr_value : str
153+
Value of the attribute to set.
154+
Default: 'd99ext9'
155+
156+
Returns
157+
-------
158+
dict
159+
Response dictionary containing:
160+
161+
- success (bool): Whether the operation succeeded
162+
- message (str): Human-readable status message
163+
- timestamp (str): ISO format timestamp
164+
- record (dict): The modified dscheck record with cindex
165+
- error (str, optional): Error message if operation failed
166+
167+
Raises
168+
------
169+
ValueError
170+
If data_file_relpath is an absolute path instead of relative.
171+
Exception
172+
Database operation failures are caught and returned in response.
173+
174+
Examples
175+
--------
176+
>>> curl -X POST https://api_url/dscheck/metadata \\
177+
... -H "Content-Type: application/json" \\
178+
... -d '{
179+
... "data_file_relpath": "exchange_subfolder/test.nc",
180+
... "attr_name": "gdex_dsid",
181+
... "attr_value": "d99ext9"
182+
... }'
183+
{"success": true, "message": "Successfully added dscheck record...", ...}
184+
"""
185+
# Initialize dictionary with None values for all expected fields (can extend as needed)
186+
dict_dscheck_post = {
187+
'command': None,
188+
'specialist': None,
189+
'argv': None,
190+
'workdir': None,
191+
}
192+
193+
# SECURITY: Define data file path with validation
194+
base_dir = Path('/gdex/data/exchange/').resolve()
195+
data_file_path = (base_dir / request.data_file_relpath).resolve()
196+
197+
# Verify final resolved path is within base directory (prevent path traversal)
198+
try:
199+
data_file_path.relative_to(base_dir)
200+
except ValueError:
201+
return {
202+
"success": False,
203+
"message": "Access denied: Path escapes base directory",
204+
"timestamp": datetime.now().isoformat(),
205+
"record": dict_dscheck_post,
206+
"error": "Path traversal attempt blocked"
207+
}
208+
209+
try:
210+
# Hardcoded shell script for now as specified
211+
command = 'add_global_attr_av.sh'
212+
specialist = "chiaweih"
213+
214+
# Use relative paths with pathlib.Path
215+
workdir_path = Path('/glade/u/home/chiaweih/data_curation_script/')
216+
argv = f"{data_file_path} {request.attr_name} {request.attr_value}"
217+
workdir = str(workdir_path)
218+
219+
# Update dictionary with hardcoded values
220+
dict_dscheck_post['command'] = command
221+
dict_dscheck_post['specialist'] = specialist
222+
dict_dscheck_post['argv'] = argv
223+
dict_dscheck_post['workdir'] = workdir
224+
225+
# Create PgDBI instance and query single record using pgget
226+
db = PgDBI()
227+
cindex = db.pgadd("dscheck", dict_dscheck_post, PgLOG.EXITLG|PgLOG.AUTOID|PgLOG.DODFLT)
228+
229+
# check output status and return appropriate response
230+
if cindex > 0:
231+
dict_dscheck_post['cindex'] = cindex
232+
return {
233+
"success": True,
234+
"message": f"Successfully added dscheck record with cindex '{cindex}'",
235+
"timestamp": datetime.now().isoformat(),
236+
"record": dict_dscheck_post
237+
}
238+
else:
239+
log = PgLOG()
240+
log.pglog("Fail to add dscheck record for '{}'".format(dict_dscheck_post['command']), logact=PgLOG.RETMSG)
241+
return {
242+
"success": False,
243+
"message": "Failed to add dscheck information, no cindex returned",
244+
"timestamp": datetime.now().isoformat(),
245+
"record": dict_dscheck_post,
246+
"error": "Failed to add dscheck record"
247+
}
248+
249+
except Exception as e:
250+
error_msg = str(e)
251+
return {
252+
"success": False,
253+
"message": "Failed to add dscheck information, during exception",
254+
"timestamp": datetime.now().isoformat(),
255+
"record": dict_dscheck_post,
256+
"error": error_msg
257+
}
258+
91259
@router.get("/health")
92260
async def health_check() -> Dict[str, Any]:
93261
"""Simple health check for the test API."""

0 commit comments

Comments
 (0)