Skip to content

Commit

Permalink
Fix issue 93 Windows blob upload (#94)
Browse files Browse the repository at this point in the history
* Fix issue 93 Windows blob upload

Signed-off-by: Sunny Carter <[email protected]>
  • Loading branch information
sunnycarter authored Jul 6, 2023
1 parent de18e3f commit 8c1f7cc
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are:
The versions coincide with releases on pip. Only major versions will be released as tags on Github.

## [0.0.x](https://github.com/oras-project/oras-py/tree/main) (0.0.x)
- patch fix for blob upload Windows, closes issue [93](https://github.com/oras-project/oras-py/issues/93) (0.1.19)
- patch fix for empty manifest config on Windows, closes issue [90](https://github.com/oras-project/oras-py/issues/90) (0.1.18)
- patch fix to correct session url pattern, closes issue [78](https://github.com/oras-project/oras-py/issues/78) (0.1.17)
- add support for tag deletion and retry decorators (0.1.16)
Expand Down
21 changes: 14 additions & 7 deletions oras/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import oras.schemas
import oras.utils
from oras.logger import logger
from oras.utils.fileio import PathAndOptionalContent

# container type can be string or container
container_type = Union[str, oras.container.Container]
Expand Down Expand Up @@ -164,9 +165,9 @@ def _validate_path(self, path: str) -> bool:
"""
return os.getcwd() in os.path.abspath(path)

def _parse_manifest_ref(self, ref: str) -> Union[Tuple[str, str], List[str]]:
def _parse_manifest_ref(self, ref: str) -> Tuple[str, str]:
"""
Parse an optional manifest config, e.g:
Parse an optional manifest config.
Examples
--------
Expand All @@ -176,10 +177,13 @@ def _parse_manifest_ref(self, ref: str) -> Union[Tuple[str, str], List[str]]:
:param ref: the manifest reference to parse (examples above)
:type ref: str
:return - A Tuple of the path and the content-type, using the default unknown
config media type if none found in the reference
"""
if ":" not in ref:
return ref, oras.defaults.unknown_config_media_type
return ref.split(":", 1)
path_content: PathAndOptionalContent = oras.utils.split_path_and_content(ref)
if not path_content.content:
path_content.content = oras.defaults.unknown_config_media_type
return path_content.path, path_content.content

def upload_blob(
self,
Expand Down Expand Up @@ -637,8 +641,11 @@ def push(self, *args, **kwargs) -> requests.Response:
# Upload files as blobs
for blob in kwargs.get("files", []):
# You can provide a blob + content type
if ":" in str(blob):
blob, media_type = str(blob).split(":", 1)
path_content: PathAndOptionalContent = oras.utils.split_path_and_content(
str(blob)
)
blob = path_content.path
media_type = path_content.content

# Must exist
if not os.path.exists(blob):
Expand Down
36 changes: 36 additions & 0 deletions oras/tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pytest

import oras.client
import oras.defaults
import oras.provider
import oras.utils

Expand Down Expand Up @@ -80,3 +81,38 @@ def test_annotated_registry_push(tmp_path):
res = client.push(
files=[artifact], target=target, annotation_file=annotation_file
)


def test_parse_manifest():
"""
Test parse manifest function.
Parse manifest function has additional logic for Windows - this isn't included in
these tests as they don't usually run on Windows.
"""
testref = "path/to/config:application/vnd.oci.image.config.v1+json"
remote = oras.provider.Registry(hostname=registry, insecure=True)
ref, content_type = remote._parse_manifest_ref(testref)
assert ref == "path/to/config"
assert content_type == "application/vnd.oci.image.config.v1+json"

testref = "path/to/config:application/vnd.oci.image.config.v1+json:extra"
remote = oras.provider.Registry(hostname=registry, insecure=True)
ref, content_type = remote._parse_manifest_ref(testref)
assert ref == "path/to/config"
assert content_type == "application/vnd.oci.image.config.v1+json:extra"

testref = "/dev/null:application/vnd.oci.image.manifest.v1+json"
ref, content_type = remote._parse_manifest_ref(testref)
assert ref == "/dev/null"
assert content_type == "application/vnd.oci.image.manifest.v1+json"

testref = "/dev/null"
ref, content_type = remote._parse_manifest_ref(testref)
assert ref == "/dev/null"
assert content_type == oras.defaults.unknown_config_media_type

testref = "path/to/config.json"
ref, content_type = remote._parse_manifest_ref(testref)
assert ref == "path/to/config.json"
assert content_type == oras.defaults.unknown_config_media_type
28 changes: 28 additions & 0 deletions oras/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,31 @@ def test_print_json():
print("Testing utils.print_json")
result = utils.print_json({1: 1})
assert result == '{\n "1": 1\n}'


def test_split_path_and_content():
"""
Test split path and content function.
Function has additional logic for Windows - this isn't included in these tests as
they don't usually run on Windows.
"""
testref = "path/to/config:application/vnd.oci.image.config.v1+json"
path_content = utils.split_path_and_content(testref)
assert path_content.path == "path/to/config"
assert path_content.content == "application/vnd.oci.image.config.v1+json"

testref = "/dev/null:application/vnd.oci.image.config.v1+json"
path_content = utils.split_path_and_content(testref)
assert path_content.path == "/dev/null"
assert path_content.content == "application/vnd.oci.image.config.v1+json"

testref = "/dev/null"
path_content = utils.split_path_and_content(testref)
assert path_content.path == "/dev/null"
assert not path_content.content

testref = "path/to/config.json"
path_content = utils.split_path_and_content(testref)
assert path_content.path == "path/to/config.json"
assert not path_content.content
1 change: 1 addition & 0 deletions oras/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
readline,
recursive_find,
sanitize_path,
split_path_and_content,
workdir,
write_file,
write_json,
Expand Down
57 changes: 57 additions & 0 deletions oras/utils/fileio.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
from typing import Generator, Optional, TextIO, Union


class PathAndOptionalContent:
"""Class for holding a path reference and optional content parsed from a string."""

def __init__(self, path: str, content: Optional[str] = None):
self.path = path
self.content = content


def make_targz(source_dir: str, dest_name: Optional[str] = None) -> str:
"""
Make a targz (compressed) archive from a source directory.
Expand Down Expand Up @@ -315,3 +323,52 @@ def read_json(filename: str, mode: str = "r") -> dict:
:type mode: str
"""
return json.loads(read_file(filename))


def split_path_and_content(ref: str) -> PathAndOptionalContent:
"""
Parse a string containing a path and an optional content
Examples
--------
<path>:<content-type>
path/to/config:application/vnd.oci.image.config.v1+json
/dev/null:application/vnd.oci.image.config.v1+json
C:\\myconfig:application/vnd.oci.image.config.v1+json
Or,
<path>
/dev/null
C:\\myconfig
:param ref: the manifest reference to parse (examples above)
:type ref: str
: return: A Tuple of the path in the reference, and the content-type if one found,
otherwise None.
"""
if ":" not in ref:
return PathAndOptionalContent(ref, None)

if pathlib.Path(ref).drive:
# Running on Windows and Path has Windows drive letter in it, it definitely has
# one colon and could have two or feasibly more, e.g.
# C:\test.tar
# C:\test.tar:application/vnd.oci.image.layer.v1.tar
# C:\test.tar:application/vnd.oci.image.layer.v1.tar:somethingelse
#
# This regex matches two colons in the string and returns everything before
# the second colon as the "path" group and everything after the second colon
# as the "context" group.
# i.e.
# (C:\test.tar):(application/vnd.oci.image.layer.v1.tar)
# (C:\test.tar):(application/vnd.oci.image.layer.v1.tar:somethingelse)
# But C:\test.tar along will not match and we just return it as is.
path_and_content = re.search(r"(?P<path>.*?:.*?):(?P<content>.*)", ref)
if path_and_content:
return PathAndOptionalContent(
path_and_content.group("path"), path_and_content.group("content")
)
return PathAndOptionalContent(ref, None)
else:
path_content_list = ref.split(":", 1)
return PathAndOptionalContent(path_content_list[0], path_content_list[1])
2 changes: 1 addition & 1 deletion oras/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
__copyright__ = "Copyright The ORAS Authors."
__license__ = "Apache-2.0"

__version__ = "0.1.18"
__version__ = "0.1.19"
AUTHOR = "Vanessa Sochat"
EMAIL = "[email protected]"
NAME = "oras"
Expand Down

0 comments on commit 8c1f7cc

Please sign in to comment.