diff --git a/app.py b/app.py index ea89649..371c942 100644 --- a/app.py +++ b/app.py @@ -1905,7 +1905,49 @@ def proxy_hapi(subpath): except Exception as e: logger.error(f"Unexpected proxy error for {final_url}: {str(e)}", exc_info=True) return jsonify({'resourceType': 'OperationOutcome', 'issue': [{'severity': 'error', 'code': 'exception', 'diagnostics': 'An unexpected error occurred within the FHIR proxy.', 'details': {'text': str(e)}}]}), 500 - # --- End of corrected proxy_hapi function --- + + +@app.route('/api/stream-retrieve', methods=['GET']) +def stream_retrieve_bundles(): + """ + Handles streaming FHIR bundle retrieval. This endpoint directly calls the service function, + bypassing the proxy to avoid conflicts. It receives the target URL, + resources, and other parameters from the front-end via URL query parameters. + """ + def generate_stream(): + # Push the application context manually for the generator's lifetime + with app.app_context(): + # Extract parameters from query string + fhir_server_url = request.args.get('fhir_server_url') + resources = request.args.getlist('resources') + validate_references = request.args.get('validate_references', 'false').lower() == 'true' + fetch_reference_bundles = request.args.get('fetch_reference_bundles', 'false').lower() == 'true' + + # Extract authentication headers + auth_token = request.headers.get('Authorization') + auth_type = 'bearer' if auth_token and auth_token.lower().startswith('bearer') else 'basic' if auth_token and auth_token.lower().startswith('basic') else 'none' + + temp_dir = tempfile.gettempdir() + zip_filename = f"retrieved_bundles_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.zip" + output_zip = os.path.join(temp_dir, zip_filename) + + try: + yield from services.retrieve_bundles( + fhir_server_url=fhir_server_url, + resources=resources, + output_zip=output_zip, + validate_references=validate_references, + fetch_reference_bundles=fetch_reference_bundles, + auth_type=auth_type, + auth_token=auth_token + ) + except Exception as e: + logger.error(f"Error in retrieve_bundles: {e}", exc_info=True) + yield json.dumps({"type": "error", "message": f"Unexpected error: {str(e)}"}) + "\n" + + response = Response(generate_stream(), mimetype='application/x-ndjson') + response.headers['X-Zip-Path'] = os.path.join('/tmp', zip_filename) + return response @app.route('/api/load-ig-to-hapi', methods=['POST']) @@ -2338,7 +2380,6 @@ def retrieve_split_data(): now=datetime.datetime.now(), app_mode=app.config['APP_MODE'], api_key=app.config['API_KEY']) - @app.route('/api/retrieve-bundles', methods=['POST']) @csrf.exempt @swag_from({ diff --git a/services.py b/services.py index c494d1c..4361b4e 100644 --- a/services.py +++ b/services.py @@ -4583,13 +4583,11 @@ def retrieve_bundles(fhir_server_url, resources, output_zip, validate_references else: yield json.dumps({"type": "info", "message": "Reference fetching OFF"}) + "\n" - # Determine Base URL and Headers for the requests. + # Determine the final base URL for requests. + final_base_url = fhir_server_url.rstrip('/') if fhir_server_url and fhir_server_url != '/fhir' else app.config.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir') headers = {'Accept': 'application/fhir+json, application/fhir+xml;q=0.9, */*;q=0.8'} is_custom_url = fhir_server_url != '/fhir' and fhir_server_url is not None and fhir_server_url.startswith('http') - - final_base_url = fhir_server_url.rstrip('/') if fhir_server_url else '/fhir' - if is_custom_url: if auth_type in ['bearer', 'basic'] and auth_token: auth_display = 'Basic ' if auth_type == 'basic' else (auth_token[:10] + '...' if len(auth_token) > 10 else auth_token) @@ -4600,7 +4598,7 @@ def retrieve_bundles(fhir_server_url, resources, output_zip, validate_references logger.debug(f"Will send requests directly to: {final_base_url}") else: yield json.dumps({"type": "info", "message": "Using no authentication for local HAPI server"}) + "\n" - logger.debug("Will use proxy targeting local HAPI server") + logger.debug("Will use local HAPI server") # Fetch Initial Bundles initial_bundle_files = [] diff --git a/templates/retrieve_split_data.html b/templates/retrieve_split_data.html index ef25795..b47b925 100644 --- a/templates/retrieve_split_data.html +++ b/templates/retrieve_split_data.html @@ -383,8 +383,8 @@ document.addEventListener('DOMContentLoaded', () => { if (icon) icon.style.display = 'inline-block'; return; } - - const currentFhirServerUrl = useLocalHapi ? '/fhir' : fhirServerUrlInput.value.trim().replace(/\/+$/, ''); + + const currentFhirServerUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim().replace(/\/+$/, ''); if (!useLocalHapi && !currentFhirServerUrl) { alert('Custom FHIR Server URL is required.'); fhirServerUrlInput.classList.add('is-invalid'); @@ -407,7 +407,7 @@ document.addEventListener('DOMContentLoaded', () => { if (currentFhirServerUrl) { url.searchParams.set('proxy-target', currentFhirServerUrl); } - selectedResources.forEach(res => url.searchParams.append('resource_type', res)); + selectedResources.forEach(res => url.searchParams.append('resources', res)); url.searchParams.set('validate_references', validateReferences); url.searchParams.set('fetch_reference_bundles', fetchReferenceBundles); @@ -428,7 +428,7 @@ document.addEventListener('DOMContentLoaded', () => { console.log(`Submitting retrieve request. URL: ${url.toString()}, Headers: ${JSON.stringify(headers)}`); try { - const response = await fetch(url.toString(), { method: 'POST', headers: headers }); + const response = await fetch(url.toString(), { method: 'GET', headers: headers }); if (!response.ok) { const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response.' })); @@ -647,25 +647,23 @@ document.addEventListener('DOMContentLoaded', () => { link.download = 'split_resources.zip'; document.body.appendChild(link); link.click(); - document.body.removeChild(link); - setTimeout(() => { - const csrfToken = splitForm.querySelector('input[name="csrf_token"]')?.value || ''; - fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }}) - .then(() => console.log("Session clear requested after split download.")) - .catch(err => console.error("Session clear failed:", err)); - downloadSplitButton.style.display = 'none'; - splitZipPath = null; - }, 500); - } else { - console.error("No split ZIP path available for download"); - alert("Download error: No file path available."); - } - }); - - // --- Initial Setup Calls --- - updateBundleSourceUI(); - updateServerToggleUI(); - toggleFetchReferenceBundles(); +document.body.removeChild(link); +setTimeout(() => { +const csrfToken = splitForm.querySelector('input[name="csrf_token"]')?.value || ''; +fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }}) +.then(() => console.log("Session clear requested after split download.")) +.catch(err => console.error("Session clear failed:", err)); +downloadSplitButton.style.display = 'none'; +splitZipPath = null; +}, 500); +} else { +console.error("No split ZIP path available for download"); +alert("Download error: No file path available."); +} +}); +updateBundleSourceUI(); +updateServerToggleUI(); +toggleFetchReferenceBundles(); }); {% endblock %} \ No newline at end of file