Skip to content

Commit c7c6f8a

Browse files
authored
Merge pull request #165 from ynput/enhancement/add-upload-reviewable
Upload reviewable method
2 parents 1bdc77c + 4e80db2 commit c7c6f8a

File tree

4 files changed

+211
-0
lines changed

4 files changed

+211
-0
lines changed

ayon_api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
download_file,
6868
upload_file_from_stream,
6969
upload_file,
70+
upload_reviewable,
7071
trigger_server_restart,
7172
query_graphql,
7273
get_graphql_schema,
@@ -290,6 +291,7 @@
290291
"download_file",
291292
"upload_file_from_stream",
292293
"upload_file",
294+
"upload_reviewable",
293295
"trigger_server_restart",
294296
"query_graphql",
295297
"get_graphql_schema",

ayon_api/_api.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,29 @@ def upload_file(*args, **kwargs):
938938
return con.upload_file(*args, **kwargs)
939939

940940

941+
def upload_reviewable(*args, **kwargs):
942+
"""Upload reviewable file to server.
943+
944+
Args:
945+
project_name (str): Project name.
946+
version_id (str): Version id.
947+
filepath (str): Reviewable file path to upload.
948+
label (Optional[str]): Reviewable label. Filled automatically
949+
server side with filename.
950+
content_type (Optional[str]): MIME type of the file.
951+
filename (Optional[str]): User as original filename. Filename from
952+
'filepath' is used when not filled.
953+
progress (Optional[TransferProgress]): Progress.
954+
headers (Optional[Dict[str, Any]]): Headers.
955+
956+
Returns:
957+
RestApiResponse: Server response.
958+
959+
"""
960+
con = get_server_api_connection()
961+
return con.upload_reviewable(*args, **kwargs)
962+
963+
941964
def trigger_server_restart():
942965
"""Trigger server restart.
943966

ayon_api/server_api.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
get_default_settings_variant,
9191
get_default_site_id,
9292
NOT_SET,
93+
get_media_mime_type,
9394
)
9495

9596
PatternType = type(re.compile(""))
@@ -1937,6 +1938,77 @@ def upload_file(
19371938
endpoint, stream, progress, request_type, **kwargs
19381939
)
19391940

1941+
def upload_reviewable(
1942+
self,
1943+
project_name,
1944+
version_id,
1945+
filepath,
1946+
label=None,
1947+
content_type=None,
1948+
filename=None,
1949+
progress=None,
1950+
headers=None,
1951+
**kwargs
1952+
):
1953+
"""Upload reviewable file to server.
1954+
1955+
Args:
1956+
project_name (str): Project name.
1957+
version_id (str): Version id.
1958+
filepath (str): Reviewable file path to upload.
1959+
label (Optional[str]): Reviewable label. Filled automatically
1960+
server side with filename.
1961+
content_type (Optional[str]): MIME type of the file.
1962+
filename (Optional[str]): User as original filename. Filename from
1963+
'filepath' is used when not filled.
1964+
progress (Optional[TransferProgress]): Progress.
1965+
headers (Optional[Dict[str, Any]]): Headers.
1966+
1967+
Returns:
1968+
RestApiResponse: Server response.
1969+
1970+
"""
1971+
if not content_type:
1972+
content_type = get_media_mime_type(filepath)
1973+
1974+
if not content_type:
1975+
raise ValueError(
1976+
f"Could not determine MIME type of file '{filepath}'"
1977+
)
1978+
1979+
if headers is None:
1980+
headers = self.get_headers(content_type)
1981+
else:
1982+
# Make sure content-type is filled with file content type
1983+
content_type_key = next(
1984+
(
1985+
key
1986+
for key in headers
1987+
if key.lower() == "content-type"
1988+
),
1989+
"Content-Type"
1990+
)
1991+
headers[content_type_key] = content_type
1992+
1993+
# Fill original filename if not explicitly defined
1994+
if not filename:
1995+
filename = os.path.basename(filepath)
1996+
headers["x-file-name"] = filename
1997+
1998+
query = f"?label={label}" if label else ""
1999+
endpoint = (
2000+
f"/projects/{project_name}"
2001+
f"/versions/{version_id}/reviewables{query}"
2002+
)
2003+
return self.upload_file(
2004+
endpoint,
2005+
filepath,
2006+
progress=progress,
2007+
headers=headers,
2008+
request_type=RequestTypes.post,
2009+
**kwargs
2010+
)
2011+
19402012
def trigger_server_restart(self):
19412013
"""Trigger server restart.
19422014

ayon_api/utils.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import platform
77
import collections
88
from urllib.parse import urlparse, urlencode
9+
from typing import Optional
910

1011
import requests
1112
import unidecode
@@ -704,3 +705,116 @@ def create_dependency_package_basename(platform_name=None):
704705
now_date = datetime.datetime.now()
705706
time_stamp = now_date.strftime("%y%m%d%H%M")
706707
return "ayon_{}_{}".format(time_stamp, platform_name)
708+
709+
710+
711+
def _get_media_mime_type_from_ftyp(content):
712+
if content[8:10] == b"qt" or content[8:12] == b"MSNV":
713+
return "video/quicktime"
714+
715+
if content[8:12] in (b"3g2a", b"3g2b", b"3g2c", b"KDDI"):
716+
return "video/3gpp2"
717+
718+
if content[8:12] in (
719+
b"isom", b"iso2", b"avc1", b"F4V", b"F4P", b"F4A", b"F4B", b"mmp4",
720+
# These might be "video/mp4v"
721+
b"mp41", b"mp42",
722+
# Nero
723+
b"NDSC", b"NDSH", b"NDSM", b"NDSP", b"NDSS", b"NDXC", b"NDXH",
724+
b"NDXM", b"NDXP", b"NDXS",
725+
):
726+
return "video/mp4"
727+
728+
if content[8:12] in (
729+
b"3ge6", b"3ge7", b"3gg6",
730+
b"3gp1", b"3gp2", b"3gp3", b"3gp4", b"3gp5", b"3gp6", b"3gs7",
731+
):
732+
return "video/3gpp"
733+
734+
if content[8:11] == b"JP2":
735+
return "image/jp2"
736+
737+
if content[8:11] == b"jpm":
738+
return "image/jpm"
739+
740+
if content[8:11] == b"jpx":
741+
return "image/jpx"
742+
743+
if content[8:12] in (b"M4V\x20", b"M4VH", b"M4VP"):
744+
return "video/x-m4v"
745+
746+
if content[8:12] in (b"mj2s", b"mjp2"):
747+
return "video/mj2"
748+
return None
749+
750+
751+
def get_media_mime_type_for_content(content: bytes) -> Optional[str]:
752+
content_len = len(content)
753+
# Pre-validation (largest definition check)
754+
# - hopefully there cannot be media defined in less than 12 bytes
755+
if content_len < 12:
756+
return None
757+
758+
# FTYP
759+
if content[4:8] == b"ftyp":
760+
return _get_media_mime_type_from_ftyp(content)
761+
762+
# BMP
763+
if content[0:2] == b"BM":
764+
return "image/bmp"
765+
766+
# Tiff
767+
if content[0:2] in (b"MM", b"II"):
768+
return "tiff"
769+
770+
# PNG
771+
if content[0:4] == b"\211PNG":
772+
return "image/png"
773+
774+
# SVG
775+
if b'xmlns="http://www.w3.org/2000/svg"' in content:
776+
return "image/svg+xml"
777+
778+
# JPEG, JFIF or Exif
779+
if (
780+
content[0:4] == b"\xff\xd8\xff\xdb"
781+
or content[6:10] in (b"JFIF", b"Exif")
782+
):
783+
return "image/jpeg"
784+
785+
# Webp
786+
if content[0:4] == b"RIFF" and content[8:12] == b"WEBP":
787+
return "image/webp"
788+
789+
# Gif
790+
if content[0:6] in (b"GIF87a", b"GIF89a"):
791+
return "gif"
792+
793+
# Adobe PhotoShop file (8B > Adobe, PS > PhotoShop)
794+
if content[0:4] == b"8BPS":
795+
return "image/vnd.adobe.photoshop"
796+
797+
# Windows ICO > this might be wild guess as multiple files can start
798+
# with this header
799+
if content[0:4] == b"\x00\x00\x01\x00":
800+
return "image/x-icon"
801+
return None
802+
803+
804+
def get_media_mime_type(filepath: str) -> Optional[str]:
805+
"""Determine Mime-Type of a file.
806+
807+
Args:
808+
filepath (str): Path to file.
809+
810+
Returns:
811+
Optional[str]: Mime type or None if is unknown mime type.
812+
813+
"""
814+
if not filepath or not os.path.exists(filepath):
815+
return None
816+
817+
with open(filepath, "rb") as stream:
818+
content = stream.read()
819+
820+
return get_media_mime_type_for_content(content)

0 commit comments

Comments
 (0)