From 745e61438d3742e9a11fed3f8987e9a22f67bded Mon Sep 17 00:00:00 2001 From: Doug Halley Date: Fri, 13 Oct 2023 09:48:53 -0400 Subject: [PATCH] Adds support to `filepath_from_url` to support non-encoded URLs with Windows drive letters * Add support to `filepath_from_url` to support non-encoded URLs with Windows drive letters * Added additional unit tests Signed-off-by: Doug Halley --- .../opentimelineio/url_utils.py | 36 ++++++++++++++++--- tests/test_url_conversions.py | 9 +++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/py-opentimelineio/opentimelineio/url_utils.py b/src/py-opentimelineio/opentimelineio/url_utils.py index 9e1c02520..61e44be58 100644 --- a/src/py-opentimelineio/opentimelineio/url_utils.py +++ b/src/py-opentimelineio/opentimelineio/url_utils.py @@ -9,7 +9,10 @@ parse as urlparse, request ) -import pathlib +from pathlib import ( + Path, + PureWindowsPath +) def url_from_filepath(fpath): @@ -18,7 +21,7 @@ def url_from_filepath(fpath): try: # appears to handle absolute windows paths better, which are absolute # and start with a drive letter. - return urlparse.unquote(pathlib.Path(fpath).as_uri()) + return urlparse.unquote(Path(fpath).as_uri()) except ValueError: # scheme is "file" for absolute paths, else "" scheme = "file" if os.path.isabs(fpath) else "" @@ -37,7 +40,32 @@ def url_from_filepath(fpath): def filepath_from_url(urlstr): - """ Take a url and return a filepath """ + """ + Take an url and return a filepath. + + URLs can either be encoded according to the `RFC 3986`_ standard or not. + Additionally, Windows mapped paths need to be accounted for when processing a + URL; however, there are `ongoing discussions`_ about how to best handle this within + Python. This function is meant to cover all of these scenarios in the interim. + .. _RFC 3986: https://tools.ietf.org/html/rfc3986#section-2.1 + .. _ongoing discussions: https://discuss.python.org/t/file-uris-in-python/15600 + """ + + # Parse provided URL parsed_result = urlparse.urlparse(urlstr) - return request.url2pathname(parsed_result.path) + + # Convert the parsed URL to a path + filepath = Path(request.url2pathname(parsed_result.path)) + + # If the network location is a window drive, reassemble the path + if PureWindowsPath(parsed_result.netloc).drive: + filepath = Path(parsed_result.netloc + parsed_result.path) + + # Otherwise check if the specified index is a windows drive, then offset the path + elif PureWindowsPath(filepath.parts[1]).drive: + # Remove leading "/" if/when `request.url2pathname` yields "/S:/path/file.ext" + filepath = filepath.relative_to(filepath.root) + + # Convert "\" to "/" if needed + return filepath.as_posix() diff --git a/tests/test_url_conversions.py b/tests/test_url_conversions.py index 5e994148b..6a2e8d63c 100644 --- a/tests/test_url_conversions.py +++ b/tests/test_url_conversions.py @@ -32,6 +32,10 @@ MEDIA_EXAMPLE_PATH_ABS ) +ENCODED_WINDOWS_URL = "file://localhost/S%3a/path/file.ext" +WINDOWS_URL = "file://S:/path/file.ext" +CORRECTED_WINDOWS_PATH = "S:/path/file.ext" + class TestConversions(unittest.TestCase): def test_roundtrip_abs(self): @@ -51,6 +55,11 @@ def test_roundtrip_rel(self): # should have reconstructed it by this point self.assertEqual(os.path.normpath(result), MEDIA_EXAMPLE_PATH_REL) + def test_windows_urls(self): + for url in (ENCODED_WINDOWS_URL, WINDOWS_URL): + processed_url = otio.url_utils.filepath_from_url(url) + self.assertEqual(processed_url, CORRECTED_WINDOWS_PATH) + if __name__ == "__main__": unittest.main()