Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/py-opentimelineio/opentimelineio-bindings/otio_bundle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ Options for reading bundles.
"Convert the media reference paths to absolute paths. "
"If this is set to true for otioz files, an extract_path must also be set.");

mbundle.def(
"file_from_url",
&file_from_url,
"Convert a file:// URL to a filesystem path.\n\n"
"Handles Windows drive letters in netloc position (file://C:/...), "
"UNC paths (file://host/share/...), percent-encoded characters, "
"and the 'localhost' authority. Returns the input unchanged if it "
"is a bare path with no URL scheme. Returns None if the URL uses a "
"non-file scheme (e.g. http://...).",
py::arg("url"));

mbundle.def(
"dry_run",
[](
Expand Down
1 change: 1 addition & 0 deletions src/py-opentimelineio/opentimelineio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
adapters,
hooks,
algorithms,
url_utils,
versioning,
)
61 changes: 61 additions & 0 deletions src/py-opentimelineio/opentimelineio/url_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the OpenTimelineIO project

"""Utilities for conversion between URLs and file paths."""

import os
import urllib
import pathlib

from ._otio import bundle


def url_from_filepath(fpath):
"""Convert a filesystem path to a URL in a portable way.

ensures that `fpath` conforms to the following pattern:
* if it is an absolute path, "file:///path/to/thing"
* if it is a relative path, "path/to/thing"

In other words, if you pass in:
* "/var/tmp/thing.otio" -> "file:///var/tmp/thing.otio"
* "tmp/thing.otio" -> "tmp/thing.otio"
"""

try:
# appears to handle absolute windows paths better, which are absolute
# and start with a drive letter.
return urllib.parse.unquote(pathlib.PurePath(fpath).as_uri())
except ValueError:
# scheme is "file" for absolute paths, else ""
scheme = "file" if os.path.isabs(fpath) else ""

# handles relative paths
return urllib.parse.urlunparse(
urllib.parse.ParseResult(
scheme=scheme,
path=fpath,
netloc="",
params="",
query="",
fragment=""
)
)


def filepath_from_url(urlstr):
"""
Take a URL and return a filepath.

Handles URLs encoded per `RFC 3986`_, Windows drive letters
(``file://C:/...``), UNC paths (``file://host/share/...``), and the
``file://localhost/...`` variant.

Returns the input unchanged if it cannot be converted.

.. _RFC 3986: https://tools.ietf.org/html/rfc3986#section-2.1
"""
result = bundle.file_from_url(urlstr)
if result is None:
return urlstr
return result
64 changes: 64 additions & 0 deletions tests/test_url_conversions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the OpenTimelineIO project

""" Unit tests of functions that convert between file paths and urls. """

import unittest
import os

import opentimelineio as otio

MEDIA_EXAMPLE_PATH_REL = os.path.relpath(
os.path.join(
os.path.dirname(__file__),
"..", # root
"docs",
"_static",
"OpenTimelineIO@3xDark.png"
)
)
MEDIA_EXAMPLE_PATH_URL_REL = otio.url_utils.url_from_filepath(
MEDIA_EXAMPLE_PATH_REL
)
MEDIA_EXAMPLE_PATH_ABS = os.path.abspath(
MEDIA_EXAMPLE_PATH_REL.replace(
"3xDark",
"3xLight"
)
)
MEDIA_EXAMPLE_PATH_URL_ABS = otio.url_utils.url_from_filepath(
MEDIA_EXAMPLE_PATH_ABS
)


class TestConversions(unittest.TestCase):
def test_roundtrip_abs(self):
self.assertTrue(MEDIA_EXAMPLE_PATH_URL_ABS.startswith("file://"))
full_path = os.path.abspath(
otio.url_utils.filepath_from_url(MEDIA_EXAMPLE_PATH_URL_ABS)
)

# should have reconstructed it by this point
self.assertEqual(full_path, MEDIA_EXAMPLE_PATH_ABS)

def test_roundtrip_rel(self):
self.assertFalse(MEDIA_EXAMPLE_PATH_URL_REL.startswith("file://"))

result = otio.url_utils.filepath_from_url(MEDIA_EXAMPLE_PATH_URL_REL)

# should have reconstructed it by this point
self.assertEqual(os.path.normpath(result), MEDIA_EXAMPLE_PATH_REL)

def test_non_file_scheme_passes_through(self):
# The C++ file_from_url returns None for non-file schemes;
# the Python wrapper passes the input through unchanged, matching
# the historical Python behavior of not raising on unrecognized
# input.
self.assertEqual(
otio.url_utils.filepath_from_url("http://example.com/foo"),
"http://example.com/foo",
)


if __name__ == "__main__":
unittest.main()
Loading