Repository: openneuroorg/openneuro
Issue: URIs returned by GraphQL API not properly encoded

**Describe the bug**

The URIs returned by the GraphQL API are not properly encoded and may therefore not always be easily accessed programmatically.

**To Reproduce**

Execute the following GraphQL query on https://openneuro.org/crn/graphql:

```graphql
query {
  dataset(id: "ds000248") {
    latestSnapshot {
      id
      files(
        prefix: "derivatives/freesurfer/subjects/sub-01/mri/aparc+aseg.mgz"
      ) {
        urls
      }
    }
  }
}
```

The returned URI for this file is:
```
https://s3.amazonaws.com/openneuro.org/ds000248/derivatives/freesurfer/subjects/sub-01/mri/aparc+aseg.mgz?versionId=2Wx7w.fCYeGzGWLnW9sxWsPdztl.2HL0
```

However, trying to access it yields a 404 error. The culprit is the un-encoded `+` character in the filename. If one URL-encodes the `+`, producing:
```
https://s3.amazonaws.com/openneuro.org/ds000248/derivatives/freesurfer/subjects/sub-01/mri/aparc%2baseg.mgz?versionId=2Wx7w.fCYeGzGWLnW9sxWsPdztl.2HL0
```
the URI is valid.

**Expected behavior**
The returned URIs in the `urls` array should point to existing resources. Special characters need to be properly encoded.

**Additional info**
I also checked with the latest stable version of `openneuro-cli` and it, too, yields a 404 for above file.



Changed files:
services/datalad/tests/test_annex.py, services/datalad/datalad_service/common/annex.py

diff --git a/services/datalad/datalad_service/common/annex.py b/services/datalad/datalad_service/common/annex.py
index 49dde6614..5fa2f1aaf 100644
--- a/services/datalad/datalad_service/common/annex.py
+++ b/services/datalad/datalad_service/common/annex.py
@@ -3,6 +3,7 @@ import io
 import json
 from mmap import mmap
 import subprocess
+import urllib.parse
 
 from sentry_sdk import capture_exception
 
@@ -116,6 +117,11 @@ def read_rmet_file(remote, catFile):
     return url
 
 
+def encode_remote_url(url):
+    """S3 requires some characters to be encoded"""
+    return urllib.parse.quote_plus(url, safe="/:?")
+
+
 def get_repo_urls(path, files):
     """For each file provided, obtain the rmet data and append URLs if possible."""
     # First obtain the git-annex branch objects
@@ -165,7 +171,8 @@ def get_repo_urls(path, files):
         for path in rmetPaths:
             url = read_rmet_file(remote, catFile)
             if url:
-                rmetFiles[path]['urls'].append(url)
+                encoded_url = encode_remote_url(url)
+                rmetFiles[path]['urls'].append(encoded_url)
     return files
 
 
diff --git a/services/datalad/tests/test_annex.py b/services/datalad/tests/test_annex.py
index 892760c59..f4761a0c3 100644
--- a/services/datalad/tests/test_annex.py
+++ b/services/datalad/tests/test_annex.py
@@ -1,7 +1,7 @@
 import io
 
 from .dataset_fixtures import *
-from datalad_service.common.annex import parse_ls_tree_line, read_ls_tree_line, compute_rmet, parse_remote_line, parse_rmet_line, read_rmet_file
+from datalad_service.common.annex import parse_ls_tree_line, read_ls_tree_line, compute_rmet, parse_remote_line, parse_rmet_line, read_rmet_file, encode_remote_url
 
 expected_file_object = {
     'filename': 'dataset_description.json',
@@ -122,3 +122,8 @@ def test_read_rmet_file():
     1590213748.042921433s 57894849-d0c8-4c62-8418-3627be18a196:V +iVcEk18e3J2WQys4zr_ANaTPfpUufW4Y#ds002778/dataset_description.json""")
     url = read_rmet_file(remote, catFile)
     assert url == 'http://openneuro.org.s3.amazonaws.com/ds002778/dataset_description.json?versionId=iVcEk18e3J2WQys4zr_ANaTPfpUufW4Y'
+
+
+def test_remote_url_encoding():
+    assert encode_remote_url(
+        "https://s3.amazonaws.com/openneuro.org/ds000248/derivatives/freesurfer/subjects/sub-01/mri/aparc+aseg.mgz?versionId=2Wx7w.fCYeGzGWLnW9sxWsPdztl.2HL0") == "https://s3.amazonaws.com/openneuro.org/ds000248/derivatives/freesurfer/subjects/sub-01/mri/aparc%2Baseg.mgz?versionId%3D2Wx7w.fCYeGzGWLnW9sxWsPdztl.2HL0"