Repository: openneuroorg/openneuro
Issue: Web UI downloads failing for annexed files removed in draft

### What went wrong?

https://openneuro.org/datasets/ds001907 has 46 participants. All but 9 are removed in draft. Attempting to download an image from a removed participant in the latest snapshot fails.

Data is still on S3 and can be retrieved through DataLad.

### Expected behavior

Unpurged files should be downloadable even if removed in draft or later snapshots.

### How to reproduce

1. Go to https://openneuro.org/datasets/ds001907/versions/3.0.2
2. Attempt to view/download NIfTI from the final subject (e.g., https://openneuro.org/datasets/ds001907/versions/3.0.2/file-display/sub-RC4227:ses-1:anat:sub-RC4227_ses-1_T1w.nii.gz)
3. Get `{"error": "file not found"}`

### Desktop

- OS: Ubuntu
- Browser: Firefox
- Version: 109


### Phone

- Device:
- OS:
- Browser:
- Version:


### Additional information

_No response_

Changed files:
services/datalad/datalad_service/handlers/files.py, services/datalad/tests/test_files.py

diff --git a/services/datalad/datalad_service/handlers/files.py b/services/datalad/datalad_service/handlers/files.py
index 491f4ce1c..554bc1d9d 100644
--- a/services/datalad/datalad_service/handlers/files.py
+++ b/services/datalad/datalad_service/handlers/files.py
@@ -22,8 +22,8 @@ class FilesResource(object):
             # If the file begins with an annex path, return that path
             if file_content[0:4096].find('.git/annex') != -1:
                 # Resolve absolute path for annex target
-                target_path = os.path.join(
-                    ds_path, os.path.dirname(filename), file_content)
+                target_path = os.path.normpath(os.path.join(
+                    ds_path, os.path.dirname(filename), file_content))
                 # Verify the annex path is within the dataset dir
                 if ds_path == os.path.commonpath((ds_path, target_path)):
                     fd = open(target_path, 'rb')
diff --git a/services/datalad/tests/test_files.py b/services/datalad/tests/test_files.py
index c84ba2242..4d7c7c584 100644
--- a/services/datalad/tests/test_files.py
+++ b/services/datalad/tests/test_files.py
@@ -43,6 +43,31 @@ def test_get_annexed_file(client):
     assert result.content.decode() == file_data
 
 
+def test_get_nested_annexed_file(client):
+    # Test accessing a subdir annexed file in git history
+    ds_id = 'ds000001'
+    file_data = 'test image, please ignore'
+    # Add it
+    response = client.simulate_post(
+        '/datasets/{}/files/sub-01:anat:data.nii.gz'.format(ds_id), body=file_data)
+    assert response.status == falcon.HTTP_OK
+    response = client.simulate_post('/datasets/{}/draft'.format(ds_id))
+    commit = json.loads(response.content)['ref']
+    assert response.status == falcon.HTTP_OK
+    # Delete it
+    response = client.simulate_delete('/datasets/{}/files'.format(
+        ds_id), body='{ "filenames": ["sub-01/anat/data.nii.gz"] }')
+    assert response.status == falcon.HTTP_OK
+    response = client.simulate_post('/datasets/{}/draft'.format(ds_id))
+    assert response.status == falcon.HTTP_OK
+    # Get it
+    result = client.simulate_get(
+        '/datasets/{}/snapshots/{}/files/sub-01:anat:data.nii.gz'.format(ds_id, commit), file_wrapper=FileWrapper)
+    content_len = int(result.headers['content-length'])
+    assert content_len == len(result.content)
+    assert result.content.decode() == file_data
+
+
 def test_get_annexed_file_nested(client):
     ds_id = 'ds000001'
     file_data = 'test image, please ignore'