Shaman: allow Manager to determine the final checkout path
The checkout request now responds with the final checkout path. This makes it possible for the Manager to ensure the checkout is unique.
This commit is contained in:
parent
0e682282f0
commit
724938c7ae
@ -19,14 +19,14 @@ if TYPE_CHECKING:
|
|||||||
from ..manager import ApiClient as _ApiClient
|
from ..manager import ApiClient as _ApiClient
|
||||||
|
|
||||||
from ..manager.models import (
|
from ..manager.models import (
|
||||||
|
ShamanCheckoutResult as _ShamanCheckoutResult,
|
||||||
ShamanRequirementsRequest as _ShamanRequirementsRequest,
|
ShamanRequirementsRequest as _ShamanRequirementsRequest,
|
||||||
ShamanRequirementsResponse as _ShamanRequirementsResponse,
|
|
||||||
ShamanFileSpec as _ShamanFileSpec,
|
ShamanFileSpec as _ShamanFileSpec,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_ApiClient = object
|
_ApiClient = object
|
||||||
|
_ShamanCheckoutResult = object
|
||||||
_ShamanRequirementsRequest = object
|
_ShamanRequirementsRequest = object
|
||||||
_ShamanRequirementsResponse = object
|
|
||||||
_ShamanFileSpec = object
|
_ShamanFileSpec = object
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -58,10 +58,14 @@ class Packer(bat_pack.Packer): # type: ignore
|
|||||||
super().__init__(blendfile, project_root, target, **kwargs)
|
super().__init__(blendfile, project_root, target, **kwargs)
|
||||||
self.checkout_path = checkout_path
|
self.checkout_path = checkout_path
|
||||||
self.api_client = api_client
|
self.api_client = api_client
|
||||||
|
self.shaman_transferrer: Optional[Transferrer] = None
|
||||||
|
|
||||||
# Mypy doesn't understand that bat_transfer.FileTransferer exists.
|
# Mypy doesn't understand that bat_transfer.FileTransferer exists.
|
||||||
def _create_file_transferer(self) -> bat_transfer.FileTransferer: # type: ignore
|
def _create_file_transferer(self) -> bat_transfer.FileTransferer: # type: ignore
|
||||||
return Transferrer(self.api_client, self.project, self.checkout_path)
|
self.shaman_transferrer = Transferrer(
|
||||||
|
self.api_client, self.project, self.checkout_path
|
||||||
|
)
|
||||||
|
return self.shaman_transferrer
|
||||||
|
|
||||||
def _make_target_path(self, target: str) -> PurePath:
|
def _make_target_path(self, target: str) -> PurePath:
|
||||||
return PurePosixPath("/")
|
return PurePosixPath("/")
|
||||||
@ -70,10 +74,11 @@ class Packer(bat_pack.Packer): # type: ignore
|
|||||||
def output_path(self) -> PurePath:
|
def output_path(self) -> PurePath:
|
||||||
"""The path of the packed blend file in the target directory."""
|
"""The path of the packed blend file in the target directory."""
|
||||||
assert self._output_path is not None
|
assert self._output_path is not None
|
||||||
|
assert self.shaman_transferrer is not None
|
||||||
|
|
||||||
checkout_location = PurePosixPath(self.checkout_path)
|
checkout_root = PurePosixPath(self.shaman_transferrer.checkout_path)
|
||||||
rel_output = self._output_path.relative_to(self._target_path)
|
rel_output = self._output_path.relative_to(self._target_path)
|
||||||
out_path: PurePath = checkout_location / rel_output
|
out_path: PurePath = checkout_root / rel_output
|
||||||
return out_path
|
return out_path
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
@ -149,7 +154,10 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.log.info("All files uploaded succesfully")
|
self.log.info("All files uploaded succesfully")
|
||||||
self._request_checkout(shaman_file_specs)
|
checkout_result = self._request_checkout(shaman_file_specs)
|
||||||
|
|
||||||
|
# Update our checkout path to match the one received from the Manager.
|
||||||
|
self.checkout_path = checkout_result.checkout_path
|
||||||
|
|
||||||
def _upload_missing_files(
|
def _upload_missing_files(
|
||||||
self, shaman_file_specs: _ShamanRequirementsRequest
|
self, shaman_file_specs: _ShamanRequirementsRequest
|
||||||
@ -407,14 +415,16 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore
|
|||||||
raise self.AbortUpload("interrupting ongoing upload")
|
raise self.AbortUpload("interrupting ongoing upload")
|
||||||
super().report_transferred(bytes_transferred)
|
super().report_transferred(bytes_transferred)
|
||||||
|
|
||||||
def _request_checkout(self, shaman_file_specs: _ShamanRequirementsRequest) -> None:
|
def _request_checkout(
|
||||||
|
self, shaman_file_specs: _ShamanRequirementsRequest
|
||||||
|
) -> Optional[_ShamanCheckoutResult]:
|
||||||
"""Ask the Shaman to create a checkout of this BAT pack."""
|
"""Ask the Shaman to create a checkout of this BAT pack."""
|
||||||
|
|
||||||
if not self.checkout_path:
|
if not self.checkout_path:
|
||||||
self.log.warning("NOT requesting checkout at Shaman")
|
self.log.warning("NOT requesting checkout at Shaman")
|
||||||
return
|
return None
|
||||||
|
|
||||||
from ..manager.models import ShamanCheckout
|
from ..manager.models import ShamanCheckout, ShamanCheckoutResult
|
||||||
from ..manager.exceptions import ApiException
|
from ..manager.exceptions import ApiException
|
||||||
|
|
||||||
self.log.info(
|
self.log.info(
|
||||||
@ -427,7 +437,9 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.shaman_api.shaman_checkout(checkoutRequest)
|
result: ShamanCheckoutResult = self.shaman_api.shaman_checkout(
|
||||||
|
checkoutRequest
|
||||||
|
)
|
||||||
except ApiException as ex:
|
except ApiException as ex:
|
||||||
match ex.status:
|
match ex.status:
|
||||||
case 424: # Files were missing
|
case 424: # Files were missing
|
||||||
@ -444,9 +456,10 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore
|
|||||||
)
|
)
|
||||||
self.log.error(msg)
|
self.log.error(msg)
|
||||||
self.error_set(msg)
|
self.error_set(msg)
|
||||||
return
|
return None
|
||||||
|
|
||||||
self.log.info("Shaman created checkout at %s", self.checkout_path)
|
self.log.info("Shaman created checkout at %s", result.checkout_path)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def make_file_spec_hashable(spec: _ShamanFileSpec) -> HashableShamanFileSpec:
|
def make_file_spec_hashable(spec: _ShamanFileSpec) -> HashableShamanFileSpec:
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
__version__ = "8a97cf50-dirty"
|
__version__ = "f66594b0-dirty"
|
||||||
|
|
||||||
# import ApiClient
|
# import ApiClient
|
||||||
from flamenco.manager.api_client import ApiClient
|
from flamenco.manager.api_client import ApiClient
|
||||||
|
@ -23,6 +23,7 @@ from flamenco.manager.model_utils import ( # noqa: F401
|
|||||||
)
|
)
|
||||||
from flamenco.manager.model.error import Error
|
from flamenco.manager.model.error import Error
|
||||||
from flamenco.manager.model.shaman_checkout import ShamanCheckout
|
from flamenco.manager.model.shaman_checkout import ShamanCheckout
|
||||||
|
from flamenco.manager.model.shaman_checkout_result import ShamanCheckoutResult
|
||||||
from flamenco.manager.model.shaman_requirements_request import ShamanRequirementsRequest
|
from flamenco.manager.model.shaman_requirements_request import ShamanRequirementsRequest
|
||||||
from flamenco.manager.model.shaman_requirements_response import ShamanRequirementsResponse
|
from flamenco.manager.model.shaman_requirements_response import ShamanRequirementsResponse
|
||||||
from flamenco.manager.model.shaman_single_file_status import ShamanSingleFileStatus
|
from flamenco.manager.model.shaman_single_file_status import ShamanSingleFileStatus
|
||||||
@ -41,7 +42,7 @@ class ShamanApi(object):
|
|||||||
self.api_client = api_client
|
self.api_client = api_client
|
||||||
self.shaman_checkout_endpoint = _Endpoint(
|
self.shaman_checkout_endpoint = _Endpoint(
|
||||||
settings={
|
settings={
|
||||||
'response_type': None,
|
'response_type': (ShamanCheckoutResult,),
|
||||||
'auth': [],
|
'auth': [],
|
||||||
'endpoint_path': '/shaman/checkout/create',
|
'endpoint_path': '/shaman/checkout/create',
|
||||||
'operation_id': 'shaman_checkout',
|
'operation_id': 'shaman_checkout',
|
||||||
@ -312,7 +313,7 @@ class ShamanApi(object):
|
|||||||
async_req (bool): execute request asynchronously
|
async_req (bool): execute request asynchronously
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
ShamanCheckoutResult
|
||||||
If the method is called asynchronously, returns the request
|
If the method is called asynchronously, returns the request
|
||||||
thread.
|
thread.
|
||||||
"""
|
"""
|
||||||
|
@ -76,7 +76,7 @@ class ApiClient(object):
|
|||||||
self.default_headers[header_name] = header_value
|
self.default_headers[header_name] = header_value
|
||||||
self.cookie = cookie
|
self.cookie = cookie
|
||||||
# Set default User-Agent.
|
# Set default User-Agent.
|
||||||
self.user_agent = 'Flamenco/8a97cf50-dirty (Blender add-on)'
|
self.user_agent = 'Flamenco/f66594b0-dirty (Blender add-on)'
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
|
@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration(
|
|||||||
"OS: {env}\n"\
|
"OS: {env}\n"\
|
||||||
"Python Version: {pyversion}\n"\
|
"Python Version: {pyversion}\n"\
|
||||||
"Version of the API: 1.0.0\n"\
|
"Version of the API: 1.0.0\n"\
|
||||||
"SDK Package Version: 8a97cf50-dirty".\
|
"SDK Package Version: f66594b0-dirty".\
|
||||||
format(env=sys.platform, pyversion=sys.version)
|
format(env=sys.platform, pyversion=sys.version)
|
||||||
|
|
||||||
def get_host_settings(self):
|
def get_host_settings(self):
|
||||||
|
@ -11,7 +11,7 @@ Method | HTTP request | Description
|
|||||||
|
|
||||||
|
|
||||||
# **shaman_checkout**
|
# **shaman_checkout**
|
||||||
> shaman_checkout(shaman_checkout)
|
> ShamanCheckoutResult shaman_checkout(shaman_checkout)
|
||||||
|
|
||||||
Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint.
|
Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint.
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ import flamenco.manager
|
|||||||
from flamenco.manager.api import shaman_api
|
from flamenco.manager.api import shaman_api
|
||||||
from flamenco.manager.model.error import Error
|
from flamenco.manager.model.error import Error
|
||||||
from flamenco.manager.model.shaman_checkout import ShamanCheckout
|
from flamenco.manager.model.shaman_checkout import ShamanCheckout
|
||||||
|
from flamenco.manager.model.shaman_checkout_result import ShamanCheckoutResult
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
# Defining the host is optional and defaults to http://localhost
|
# Defining the host is optional and defaults to http://localhost
|
||||||
# See configuration.py for a list of all supported configuration parameters.
|
# See configuration.py for a list of all supported configuration parameters.
|
||||||
@ -50,7 +51,8 @@ with flamenco.manager.ApiClient() as api_client:
|
|||||||
# example passing only required values which don't have defaults set
|
# example passing only required values which don't have defaults set
|
||||||
try:
|
try:
|
||||||
# Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint.
|
# Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint.
|
||||||
api_instance.shaman_checkout(shaman_checkout)
|
api_response = api_instance.shaman_checkout(shaman_checkout)
|
||||||
|
pprint(api_response)
|
||||||
except flamenco.manager.ApiException as e:
|
except flamenco.manager.ApiException as e:
|
||||||
print("Exception when calling ShamanApi->shaman_checkout: %s\n" % e)
|
print("Exception when calling ShamanApi->shaman_checkout: %s\n" % e)
|
||||||
```
|
```
|
||||||
@ -64,7 +66,7 @@ Name | Type | Description | Notes
|
|||||||
|
|
||||||
### Return type
|
### Return type
|
||||||
|
|
||||||
void (empty response body)
|
[**ShamanCheckoutResult**](ShamanCheckoutResult.md)
|
||||||
|
|
||||||
### Authorization
|
### Authorization
|
||||||
|
|
||||||
@ -80,7 +82,7 @@ No authorization required
|
|||||||
|
|
||||||
| Status code | Description | Response headers |
|
| Status code | Description | Response headers |
|
||||||
|-------------|-------------|------------------|
|
|-------------|-------------|------------------|
|
||||||
**204** | Checkout was created succesfully. | - |
|
**200** | Checkout was created succesfully. | - |
|
||||||
**424** | There were files missing. Use `shamanCheckoutRequirements` to figure out which ones. | - |
|
**424** | There were files missing. Use `shamanCheckoutRequirements` to figure out which ones. | - |
|
||||||
**409** | Checkout already exists. | - |
|
**409** | Checkout already exists. | - |
|
||||||
**0** | unexpected error | - |
|
**0** | unexpected error | - |
|
||||||
|
@ -6,7 +6,7 @@ Set of files with their SHA256 checksum, size in bytes, and desired location in
|
|||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**files** | [**[ShamanFileSpec]**](ShamanFileSpec.md) | |
|
**files** | [**[ShamanFileSpec]**](ShamanFileSpec.md) | |
|
||||||
**checkout_path** | **str** | Path where the Manager should create this checkout, It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`. |
|
**checkout_path** | **str** | Path where the Manager should create this checkout. It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`. |
|
||||||
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
|
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
13
addon/flamenco/manager/docs/ShamanCheckoutResult.md
Normal file
13
addon/flamenco/manager/docs/ShamanCheckoutResult.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ShamanCheckoutResult
|
||||||
|
|
||||||
|
The result of a Shaman checkout.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**checkout_path** | **str** | Path where the Manager created this checkout. This can be different than what was requested, as the Manager will ensure a unique directory. The path is relative to the Shaman checkout path as configured on the Manager. |
|
||||||
|
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ class ShamanCheckout(ModelNormal):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
files ([ShamanFileSpec]):
|
files ([ShamanFileSpec]):
|
||||||
checkout_path (str): Path where the Manager should create this checkout, It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`.
|
checkout_path (str): Path where the Manager should create this checkout. It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`.
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
_check_type (bool): if True, values for parameters in openapi_types
|
_check_type (bool): if True, values for parameters in openapi_types
|
||||||
@ -200,7 +200,7 @@ class ShamanCheckout(ModelNormal):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
files ([ShamanFileSpec]):
|
files ([ShamanFileSpec]):
|
||||||
checkout_path (str): Path where the Manager should create this checkout, It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`.
|
checkout_path (str): Path where the Manager should create this checkout. It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the \"checkout ID\", but in this version it can be a path like `project-slug/scene-name/unique-ID`.
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
_check_type (bool): if True, values for parameters in openapi_types
|
_check_type (bool): if True, values for parameters in openapi_types
|
||||||
|
261
addon/flamenco/manager/model/shaman_checkout_result.py
Normal file
261
addon/flamenco/manager/model/shaman_checkout_result.py
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
"""
|
||||||
|
Flamenco manager
|
||||||
|
|
||||||
|
Render Farm manager API # noqa: E501
|
||||||
|
|
||||||
|
The version of the OpenAPI document: 1.0.0
|
||||||
|
Generated by: https://openapi-generator.tech
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import re # noqa: F401
|
||||||
|
import sys # noqa: F401
|
||||||
|
|
||||||
|
from flamenco.manager.model_utils import ( # noqa: F401
|
||||||
|
ApiTypeError,
|
||||||
|
ModelComposed,
|
||||||
|
ModelNormal,
|
||||||
|
ModelSimple,
|
||||||
|
cached_property,
|
||||||
|
change_keys_js_to_python,
|
||||||
|
convert_js_args_to_python_args,
|
||||||
|
date,
|
||||||
|
datetime,
|
||||||
|
file_type,
|
||||||
|
none_type,
|
||||||
|
validate_get_composed_info,
|
||||||
|
OpenApiModel
|
||||||
|
)
|
||||||
|
from flamenco.manager.exceptions import ApiAttributeError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ShamanCheckoutResult(ModelNormal):
|
||||||
|
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||||
|
Ref: https://openapi-generator.tech
|
||||||
|
|
||||||
|
Do not edit the class manually.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
allowed_values (dict): The key is the tuple path to the attribute
|
||||||
|
and the for var_name this is (var_name,). The value is a dict
|
||||||
|
with a capitalized key describing the allowed value and an allowed
|
||||||
|
value. These dicts store the allowed enum values.
|
||||||
|
attribute_map (dict): The key is attribute name
|
||||||
|
and the value is json key in definition.
|
||||||
|
discriminator_value_class_map (dict): A dict to go from the discriminator
|
||||||
|
variable value to the discriminator class name.
|
||||||
|
validations (dict): The key is the tuple path to the attribute
|
||||||
|
and the for var_name this is (var_name,). The value is a dict
|
||||||
|
that stores validations for max_length, min_length, max_items,
|
||||||
|
min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
|
||||||
|
inclusive_minimum, and regex.
|
||||||
|
additional_properties_type (tuple): A tuple of classes accepted
|
||||||
|
as additional properties values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
allowed_values = {
|
||||||
|
}
|
||||||
|
|
||||||
|
validations = {
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def additional_properties_type():
|
||||||
|
"""
|
||||||
|
This must be a method because a model may have properties that are
|
||||||
|
of type self, this must run after the class is loaded
|
||||||
|
"""
|
||||||
|
return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501
|
||||||
|
|
||||||
|
_nullable = False
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def openapi_types():
|
||||||
|
"""
|
||||||
|
This must be a method because a model may have properties that are
|
||||||
|
of type self, this must run after the class is loaded
|
||||||
|
|
||||||
|
Returns
|
||||||
|
openapi_types (dict): The key is attribute name
|
||||||
|
and the value is attribute type.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'checkout_path': (str,), # noqa: E501
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def discriminator():
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
attribute_map = {
|
||||||
|
'checkout_path': 'checkoutPath', # noqa: E501
|
||||||
|
}
|
||||||
|
|
||||||
|
read_only_vars = {
|
||||||
|
}
|
||||||
|
|
||||||
|
_composed_schemas = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@convert_js_args_to_python_args
|
||||||
|
def _from_openapi_data(cls, checkout_path, *args, **kwargs): # noqa: E501
|
||||||
|
"""ShamanCheckoutResult - a model defined in OpenAPI
|
||||||
|
|
||||||
|
Args:
|
||||||
|
checkout_path (str): Path where the Manager created this checkout. This can be different than what was requested, as the Manager will ensure a unique directory. The path is relative to the Shaman checkout path as configured on the Manager.
|
||||||
|
|
||||||
|
Keyword Args:
|
||||||
|
_check_type (bool): if True, values for parameters in openapi_types
|
||||||
|
will be type checked and a TypeError will be
|
||||||
|
raised if the wrong type is input.
|
||||||
|
Defaults to True
|
||||||
|
_path_to_item (tuple/list): This is a list of keys or values to
|
||||||
|
drill down to the model in received_data
|
||||||
|
when deserializing a response
|
||||||
|
_spec_property_naming (bool): True if the variable names in the input data
|
||||||
|
are serialized names, as specified in the OpenAPI document.
|
||||||
|
False if the variable names in the input data
|
||||||
|
are pythonic names, e.g. snake case (default)
|
||||||
|
_configuration (Configuration): the instance to use when
|
||||||
|
deserializing a file_type parameter.
|
||||||
|
If passed, type conversion is attempted
|
||||||
|
If omitted no type conversion is done.
|
||||||
|
_visited_composed_classes (tuple): This stores a tuple of
|
||||||
|
classes that we have traveled through so that
|
||||||
|
if we see that class again we will not use its
|
||||||
|
discriminator again.
|
||||||
|
When traveling through a discriminator, the
|
||||||
|
composed schema that is
|
||||||
|
is traveled through is added to this set.
|
||||||
|
For example if Animal has a discriminator
|
||||||
|
petType and we pass in "Dog", and the class Dog
|
||||||
|
allOf includes Animal, we move through Animal
|
||||||
|
once using the discriminator, and pick Dog.
|
||||||
|
Then in Dog, we will make an instance of the
|
||||||
|
Animal class but this time we won't travel
|
||||||
|
through its discriminator because we passed in
|
||||||
|
_visited_composed_classes = (Animal,)
|
||||||
|
"""
|
||||||
|
|
||||||
|
_check_type = kwargs.pop('_check_type', True)
|
||||||
|
_spec_property_naming = kwargs.pop('_spec_property_naming', False)
|
||||||
|
_path_to_item = kwargs.pop('_path_to_item', ())
|
||||||
|
_configuration = kwargs.pop('_configuration', None)
|
||||||
|
_visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
|
||||||
|
|
||||||
|
self = super(OpenApiModel, cls).__new__(cls)
|
||||||
|
|
||||||
|
if args:
|
||||||
|
raise ApiTypeError(
|
||||||
|
"Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
|
||||||
|
args,
|
||||||
|
self.__class__.__name__,
|
||||||
|
),
|
||||||
|
path_to_item=_path_to_item,
|
||||||
|
valid_classes=(self.__class__,),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._data_store = {}
|
||||||
|
self._check_type = _check_type
|
||||||
|
self._spec_property_naming = _spec_property_naming
|
||||||
|
self._path_to_item = _path_to_item
|
||||||
|
self._configuration = _configuration
|
||||||
|
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
|
||||||
|
|
||||||
|
self.checkout_path = checkout_path
|
||||||
|
for var_name, var_value in kwargs.items():
|
||||||
|
if var_name not in self.attribute_map and \
|
||||||
|
self._configuration is not None and \
|
||||||
|
self._configuration.discard_unknown_keys and \
|
||||||
|
self.additional_properties_type is None:
|
||||||
|
# discard variable.
|
||||||
|
continue
|
||||||
|
setattr(self, var_name, var_value)
|
||||||
|
return self
|
||||||
|
|
||||||
|
required_properties = set([
|
||||||
|
'_data_store',
|
||||||
|
'_check_type',
|
||||||
|
'_spec_property_naming',
|
||||||
|
'_path_to_item',
|
||||||
|
'_configuration',
|
||||||
|
'_visited_composed_classes',
|
||||||
|
])
|
||||||
|
|
||||||
|
@convert_js_args_to_python_args
|
||||||
|
def __init__(self, checkout_path, *args, **kwargs): # noqa: E501
|
||||||
|
"""ShamanCheckoutResult - a model defined in OpenAPI
|
||||||
|
|
||||||
|
Args:
|
||||||
|
checkout_path (str): Path where the Manager created this checkout. This can be different than what was requested, as the Manager will ensure a unique directory. The path is relative to the Shaman checkout path as configured on the Manager.
|
||||||
|
|
||||||
|
Keyword Args:
|
||||||
|
_check_type (bool): if True, values for parameters in openapi_types
|
||||||
|
will be type checked and a TypeError will be
|
||||||
|
raised if the wrong type is input.
|
||||||
|
Defaults to True
|
||||||
|
_path_to_item (tuple/list): This is a list of keys or values to
|
||||||
|
drill down to the model in received_data
|
||||||
|
when deserializing a response
|
||||||
|
_spec_property_naming (bool): True if the variable names in the input data
|
||||||
|
are serialized names, as specified in the OpenAPI document.
|
||||||
|
False if the variable names in the input data
|
||||||
|
are pythonic names, e.g. snake case (default)
|
||||||
|
_configuration (Configuration): the instance to use when
|
||||||
|
deserializing a file_type parameter.
|
||||||
|
If passed, type conversion is attempted
|
||||||
|
If omitted no type conversion is done.
|
||||||
|
_visited_composed_classes (tuple): This stores a tuple of
|
||||||
|
classes that we have traveled through so that
|
||||||
|
if we see that class again we will not use its
|
||||||
|
discriminator again.
|
||||||
|
When traveling through a discriminator, the
|
||||||
|
composed schema that is
|
||||||
|
is traveled through is added to this set.
|
||||||
|
For example if Animal has a discriminator
|
||||||
|
petType and we pass in "Dog", and the class Dog
|
||||||
|
allOf includes Animal, we move through Animal
|
||||||
|
once using the discriminator, and pick Dog.
|
||||||
|
Then in Dog, we will make an instance of the
|
||||||
|
Animal class but this time we won't travel
|
||||||
|
through its discriminator because we passed in
|
||||||
|
_visited_composed_classes = (Animal,)
|
||||||
|
"""
|
||||||
|
|
||||||
|
_check_type = kwargs.pop('_check_type', True)
|
||||||
|
_spec_property_naming = kwargs.pop('_spec_property_naming', False)
|
||||||
|
_path_to_item = kwargs.pop('_path_to_item', ())
|
||||||
|
_configuration = kwargs.pop('_configuration', None)
|
||||||
|
_visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
|
||||||
|
|
||||||
|
if args:
|
||||||
|
raise ApiTypeError(
|
||||||
|
"Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
|
||||||
|
args,
|
||||||
|
self.__class__.__name__,
|
||||||
|
),
|
||||||
|
path_to_item=_path_to_item,
|
||||||
|
valid_classes=(self.__class__,),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._data_store = {}
|
||||||
|
self._check_type = _check_type
|
||||||
|
self._spec_property_naming = _spec_property_naming
|
||||||
|
self._path_to_item = _path_to_item
|
||||||
|
self._configuration = _configuration
|
||||||
|
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
|
||||||
|
|
||||||
|
self.checkout_path = checkout_path
|
||||||
|
for var_name, var_value in kwargs.items():
|
||||||
|
if var_name not in self.attribute_map and \
|
||||||
|
self._configuration is not None and \
|
||||||
|
self._configuration.discard_unknown_keys and \
|
||||||
|
self.additional_properties_type is None:
|
||||||
|
# discard variable.
|
||||||
|
continue
|
||||||
|
setattr(self, var_name, var_value)
|
||||||
|
if var_name in self.read_only_vars:
|
||||||
|
raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
|
||||||
|
f"class with read only attributes.")
|
@ -27,6 +27,7 @@ from flamenco.manager.model.manager_configuration import ManagerConfiguration
|
|||||||
from flamenco.manager.model.registered_worker import RegisteredWorker
|
from flamenco.manager.model.registered_worker import RegisteredWorker
|
||||||
from flamenco.manager.model.security_error import SecurityError
|
from flamenco.manager.model.security_error import SecurityError
|
||||||
from flamenco.manager.model.shaman_checkout import ShamanCheckout
|
from flamenco.manager.model.shaman_checkout import ShamanCheckout
|
||||||
|
from flamenco.manager.model.shaman_checkout_result import ShamanCheckoutResult
|
||||||
from flamenco.manager.model.shaman_file_spec import ShamanFileSpec
|
from flamenco.manager.model.shaman_file_spec import ShamanFileSpec
|
||||||
from flamenco.manager.model.shaman_file_spec_with_status import ShamanFileSpecWithStatus
|
from flamenco.manager.model.shaman_file_spec_with_status import ShamanFileSpecWithStatus
|
||||||
from flamenco.manager.model.shaman_file_status import ShamanFileStatus
|
from flamenco.manager.model.shaman_file_status import ShamanFileStatus
|
||||||
|
@ -4,7 +4,7 @@ Render Farm manager API
|
|||||||
The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.0.0
|
- API version: 1.0.0
|
||||||
- Package version: 8a97cf50-dirty
|
- Package version: f66594b0-dirty
|
||||||
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
|
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
|
||||||
For more information, please visit [https://flamenco.io/](https://flamenco.io/)
|
For more information, please visit [https://flamenco.io/](https://flamenco.io/)
|
||||||
|
|
||||||
@ -102,6 +102,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [RegisteredWorker](flamenco/manager/docs/RegisteredWorker.md)
|
- [RegisteredWorker](flamenco/manager/docs/RegisteredWorker.md)
|
||||||
- [SecurityError](flamenco/manager/docs/SecurityError.md)
|
- [SecurityError](flamenco/manager/docs/SecurityError.md)
|
||||||
- [ShamanCheckout](flamenco/manager/docs/ShamanCheckout.md)
|
- [ShamanCheckout](flamenco/manager/docs/ShamanCheckout.md)
|
||||||
|
- [ShamanCheckoutResult](flamenco/manager/docs/ShamanCheckoutResult.md)
|
||||||
- [ShamanFileSpec](flamenco/manager/docs/ShamanFileSpec.md)
|
- [ShamanFileSpec](flamenco/manager/docs/ShamanFileSpec.md)
|
||||||
- [ShamanFileSpecWithStatus](flamenco/manager/docs/ShamanFileSpecWithStatus.md)
|
- [ShamanFileSpecWithStatus](flamenco/manager/docs/ShamanFileSpecWithStatus.md)
|
||||||
- [ShamanFileStatus](flamenco/manager/docs/ShamanFileStatus.md)
|
- [ShamanFileStatus](flamenco/manager/docs/ShamanFileStatus.md)
|
||||||
|
@ -291,9 +291,8 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
|||||||
assert self.job is not None
|
assert self.job is not None
|
||||||
self.log.info("Sending BAT pack to Shaman")
|
self.log.info("Sending BAT pack to Shaman")
|
||||||
|
|
||||||
# TODO: get project name from preferences/GUI.
|
# TODO: get project name from preferences/GUI and insert that here too.
|
||||||
# TODO: update Shaman API to ensure this is a unique location.
|
checkout_root = PurePosixPath(f"{self.job.name}")
|
||||||
checkout_root = PurePosixPath(f"project/{self.job.name}")
|
|
||||||
|
|
||||||
self.packthread = bat_interface.copy(
|
self.packthread = bat_interface.copy(
|
||||||
base_blendfile=blendfile,
|
base_blendfile=blendfile,
|
||||||
|
@ -84,7 +84,8 @@ type Shaman interface {
|
|||||||
|
|
||||||
// Checkout creates a directory, and symlinks the required files into it. The
|
// Checkout creates a directory, and symlinks the required files into it. The
|
||||||
// files must all have been uploaded to Shaman before calling this.
|
// files must all have been uploaded to Shaman before calling this.
|
||||||
Checkout(ctx context.Context, checkout api.ShamanCheckout) error
|
// Returns the final checkout directory, as it may be modified to ensure uniqueness.
|
||||||
|
Checkout(ctx context.Context, checkout api.ShamanCheckout) (string, error)
|
||||||
|
|
||||||
// Requirements checks a Shaman Requirements file, and returns the subset
|
// Requirements checks a Shaman Requirements file, and returns the subset
|
||||||
// containing the unknown files.
|
// containing the unknown files.
|
||||||
|
@ -412,11 +412,12 @@ func (m *MockShaman) EXPECT() *MockShamanMockRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Checkout mocks base method.
|
// Checkout mocks base method.
|
||||||
func (m *MockShaman) Checkout(arg0 context.Context, arg1 api.ShamanCheckout) error {
|
func (m *MockShaman) Checkout(arg0 context.Context, arg1 api.ShamanCheckout) (string, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "Checkout", arg0, arg1)
|
ret := m.ctrl.Call(m, "Checkout", arg0, arg1)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(string)
|
||||||
return ret0
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkout indicates an expected call of Checkout.
|
// Checkout indicates an expected call of Checkout.
|
||||||
|
@ -31,14 +31,16 @@ func (f *Flamenco) ShamanCheckout(e echo.Context) error {
|
|||||||
return sendAPIError(e, http.StatusBadRequest, "invalid format")
|
return sendAPIError(e, http.StatusBadRequest, "invalid format")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = f.shaman.Checkout(e.Request().Context(), api.ShamanCheckout(reqBody))
|
checkoutPath, err := f.shaman.Checkout(e.Request().Context(), api.ShamanCheckout(reqBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: return 409 when checkout already exists.
|
// TODO: return 409 when checkout already exists.
|
||||||
logger.Warn().Err(err).Msg("Shaman: creating checkout")
|
logger.Warn().Err(err).Msg("Shaman: creating checkout")
|
||||||
return sendAPIError(e, http.StatusInternalServerError, "unexpected error: %v", err)
|
return sendAPIError(e, http.StatusInternalServerError, "unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.String(http.StatusNoContent, "")
|
return e.JSON(http.StatusOK, api.ShamanCheckoutResult{
|
||||||
|
CheckoutPath: checkoutPath,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks a Shaman Requirements file, and reports which files are unknown.
|
// Checks a Shaman Requirements file, and reports which files are unknown.
|
||||||
|
@ -313,8 +313,12 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ShamanCheckout"
|
$ref: "#/components/schemas/ShamanCheckout"
|
||||||
responses:
|
responses:
|
||||||
"204":
|
"200":
|
||||||
description: Checkout was created succesfully.
|
description: Checkout was created succesfully.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ShamanCheckoutResult'
|
||||||
"424":
|
"424":
|
||||||
description: There were files missing. Use `shamanCheckoutRequirements` to figure out which ones.
|
description: There were files missing. Use `shamanCheckoutRequirements` to figure out which ones.
|
||||||
content:
|
content:
|
||||||
@ -795,6 +799,19 @@ components:
|
|||||||
size: 127
|
size: 127
|
||||||
path: logging.go
|
path: logging.go
|
||||||
|
|
||||||
|
ShamanCheckoutResult:
|
||||||
|
type: object
|
||||||
|
description: The result of a Shaman checkout.
|
||||||
|
properties:
|
||||||
|
"checkoutPath":
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Path where the Manager created this checkout. This can be different
|
||||||
|
than what was requested, as the Manager will ensure a unique
|
||||||
|
directory. The path is relative to the Shaman checkout path as
|
||||||
|
configured on the Manager.
|
||||||
|
required: [checkoutPath]
|
||||||
|
|
||||||
ShamanFileStatus:
|
ShamanFileStatus:
|
||||||
type: string
|
type: string
|
||||||
enum: [unknown, uploading, stored]
|
enum: [unknown, uploading, stored]
|
||||||
|
@ -1397,6 +1397,7 @@ func (r TaskUpdateResponse) StatusCode() int {
|
|||||||
type ShamanCheckoutResponse struct {
|
type ShamanCheckoutResponse struct {
|
||||||
Body []byte
|
Body []byte
|
||||||
HTTPResponse *http.Response
|
HTTPResponse *http.Response
|
||||||
|
JSON200 *ShamanCheckoutResult
|
||||||
JSON409 *Error
|
JSON409 *Error
|
||||||
JSON424 *Error
|
JSON424 *Error
|
||||||
JSONDefault *Error
|
JSONDefault *Error
|
||||||
@ -2054,6 +2055,13 @@ func ParseShamanCheckoutResponse(rsp *http.Response) (*ShamanCheckoutResponse, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||||
|
var dest ShamanCheckoutResult
|
||||||
|
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response.JSON200 = &dest
|
||||||
|
|
||||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409:
|
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409:
|
||||||
var dest Error
|
var dest Error
|
||||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||||
|
@ -18,94 +18,95 @@ import (
|
|||||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"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",
|
||||||
"FwAA//9HZfvdYFcAAA==",
|
"qeL6WBbrQz9XoAWYUaeTb7TWGDXptY+ZANCjk+N+L2H3VE4VRSX9BRFn0oetqA14X94K+Hqi39HJ8QgX",
|
||||||
|
"QpFrme83hCUW9/eliptE1nTge37dvL35vwAAAP//0IX4BlJZAAA=",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
@ -250,11 +250,17 @@ type SecurityError struct {
|
|||||||
|
|
||||||
// Set of files with their SHA256 checksum, size in bytes, and desired location in the checkout directory.
|
// Set of files with their SHA256 checksum, size in bytes, and desired location in the checkout directory.
|
||||||
type ShamanCheckout struct {
|
type ShamanCheckout struct {
|
||||||
// Path where the Manager should create this checkout, It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the "checkout ID", but in this version it can be a path like `project-slug/scene-name/unique-ID`.
|
// Path where the Manager should create this checkout. It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the "checkout ID", but in this version it can be a path like `project-slug/scene-name/unique-ID`.
|
||||||
CheckoutPath string `json:"checkoutPath"`
|
CheckoutPath string `json:"checkoutPath"`
|
||||||
Files []ShamanFileSpec `json:"files"`
|
Files []ShamanFileSpec `json:"files"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The result of a Shaman checkout.
|
||||||
|
type ShamanCheckoutResult struct {
|
||||||
|
// Path where the Manager created this checkout. This can be different than what was requested, as the Manager will ensure a unique directory. The path is relative to the Shaman checkout path as configured on the Manager.
|
||||||
|
CheckoutPath string `json:"checkoutPath"`
|
||||||
|
}
|
||||||
|
|
||||||
// Specification of a file in the Shaman storage.
|
// Specification of a file in the Shaman storage.
|
||||||
type ShamanFileSpec struct {
|
type ShamanFileSpec struct {
|
||||||
// Location of the file in the checkout
|
// Location of the file in the checkout
|
||||||
|
@ -20,7 +20,7 @@ var (
|
|||||||
validCheckoutRegexp = regexp.MustCompile(`^[^/?*:;{}\\][^?*:;{}\\]*$`)
|
validCheckoutRegexp = regexp.MustCompile(`^[^/?*:;{}\\][^?*:;{}\\]*$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Manager) Checkout(ctx context.Context, checkout api.ShamanCheckout) error {
|
func (m *Manager) Checkout(ctx context.Context, checkout api.ShamanCheckout) (string, error) {
|
||||||
logger := (*zerolog.Ctx(ctx)).With().
|
logger := (*zerolog.Ctx(ctx)).With().
|
||||||
Str("checkoutPath", checkout.CheckoutPath).Logger()
|
Str("checkoutPath", checkout.CheckoutPath).Logger()
|
||||||
logger.Debug().Msg("shaman: user requested checkout creation")
|
logger.Debug().Msg("shaman: user requested checkout creation")
|
||||||
@ -28,7 +28,7 @@ func (m *Manager) Checkout(ctx context.Context, checkout api.ShamanCheckout) err
|
|||||||
// Actually create the checkout.
|
// Actually create the checkout.
|
||||||
resolvedCheckoutInfo, err := m.PrepareCheckout(checkout.CheckoutPath)
|
resolvedCheckoutInfo, err := m.PrepareCheckout(checkout.CheckoutPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The checkout directory was created, so if anything fails now, it should be erased.
|
// The checkout directory was created, so if anything fails now, it should be erased.
|
||||||
@ -46,17 +46,17 @@ func (m *Manager) Checkout(ctx context.Context, checkout api.ShamanCheckout) err
|
|||||||
blobPath, status := m.fileStore.ResolveFile(fileSpec.Sha, int64(fileSpec.Size), filestore.ResolveStoredOnly)
|
blobPath, status := m.fileStore.ResolveFile(fileSpec.Sha, int64(fileSpec.Size), filestore.ResolveStoredOnly)
|
||||||
if status != filestore.StatusStored {
|
if status != filestore.StatusStored {
|
||||||
// Caller should upload this file before we can create the checkout.
|
// Caller should upload this file before we can create the checkout.
|
||||||
return ErrMissingFiles
|
return "", ErrMissingFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.SymlinkToCheckout(blobPath, resolvedCheckoutInfo.absolutePath, fileSpec.Path); err != nil {
|
if err := m.SymlinkToCheckout(blobPath, resolvedCheckoutInfo.absolutePath, fileSpec.Path); err != nil {
|
||||||
return fmt.Errorf("symlinking %q to checkout: %w", fileSpec.Path, err)
|
return "", fmt.Errorf("symlinking %q to checkout: %w", fileSpec.Path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkoutOK = true // Prevent the checkout directory from being erased again.
|
checkoutOK = true // Prevent the checkout directory from being erased again.
|
||||||
logger.Info().Msg("shaman: checkout created")
|
logger.Info().Msg("shaman: checkout created")
|
||||||
return nil
|
return resolvedCheckoutInfo.RelativePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValidCheckoutPath(checkoutPath string) bool {
|
func isValidCheckoutPath(checkoutPath string) bool {
|
||||||
|
@ -28,13 +28,13 @@ func TestCheckout(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := manager.Checkout(ctx, checkout)
|
actualCheckoutPath, err := manager.Checkout(ctx, checkout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("fatal error: %v", err)
|
t.Fatalf("fatal error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the symlinks of the checkout
|
// Check the symlinks of the checkout
|
||||||
coPath := path.Join(manager.checkoutBasePath, checkout.CheckoutPath)
|
coPath := path.Join(manager.checkoutBasePath, actualCheckoutPath)
|
||||||
assert.FileExists(t, path.Join(coPath, "subdir", "replacer.py"))
|
assert.FileExists(t, path.Join(coPath, "subdir", "replacer.py"))
|
||||||
assert.FileExists(t, path.Join(coPath, "feed.py"))
|
assert.FileExists(t, path.Join(coPath, "feed.py"))
|
||||||
assert.FileExists(t, path.Join(coPath, "httpstuff.py"))
|
assert.FileExists(t, path.Join(coPath, "httpstuff.py"))
|
||||||
|
@ -25,6 +25,7 @@ package checkout
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -43,7 +44,9 @@ type Manager struct {
|
|||||||
checkoutBasePath string
|
checkoutBasePath string
|
||||||
fileStore *filestore.Store
|
fileStore *filestore.Store
|
||||||
|
|
||||||
wg sync.WaitGroup
|
wg *sync.WaitGroup
|
||||||
|
|
||||||
|
checkoutUniquenessMutex *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvedCheckoutInfo contains the result of validating the Checkout ID and parsing it into a final path.
|
// ResolvedCheckoutInfo contains the result of validating the Checkout ID and parsing it into a final path.
|
||||||
@ -78,7 +81,7 @@ func NewManager(conf config.Config, fileStore *filestore.Store) *Manager {
|
|||||||
logger.Error().Err(err).Msg("unable to create checkout directory")
|
logger.Error().Err(err).Msg("unable to create checkout directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Manager{conf.CheckoutPath, fileStore, sync.WaitGroup{}}
|
return &Manager{conf.CheckoutPath, fileStore, new(sync.WaitGroup), new(sync.Mutex)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close waits for still-running touch() calls to finish, then returns.
|
// Close waits for still-running touch() calls to finish, then returns.
|
||||||
@ -101,7 +104,17 @@ func (m *Manager) pathForCheckout(requestedCheckoutPath string) (ResolvedCheckou
|
|||||||
// PrepareCheckout creates the root directory for a specific checkout.
|
// PrepareCheckout creates the root directory for a specific checkout.
|
||||||
// Returns the path relative to the checkout root directory.
|
// Returns the path relative to the checkout root directory.
|
||||||
func (m *Manager) PrepareCheckout(checkoutPath string) (ResolvedCheckoutInfo, error) {
|
func (m *Manager) PrepareCheckout(checkoutPath string) (ResolvedCheckoutInfo, error) {
|
||||||
checkoutPaths, err := m.pathForCheckout(checkoutPath)
|
// This function checks the filesystem and tries to ensure uniqueness, so it's
|
||||||
|
// important that it doesn't run simultaneously in parallel threads.
|
||||||
|
m.checkoutUniquenessMutex.Lock()
|
||||||
|
defer m.checkoutUniquenessMutex.Unlock()
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
// Just try 10 different random tokens. If that still doesn't work, fail.
|
||||||
|
for try := 0; try < 10; try++ {
|
||||||
|
randomisedPath := fmt.Sprintf("%s-%s", checkoutPath, randomisedToken())
|
||||||
|
|
||||||
|
checkoutPaths, err := m.pathForCheckout(randomisedPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ResolvedCheckoutInfo{}, err
|
return ResolvedCheckoutInfo{}, err
|
||||||
}
|
}
|
||||||
@ -113,27 +126,34 @@ func (m *Manager) PrepareCheckout(checkoutPath string) (ResolvedCheckoutInfo, er
|
|||||||
|
|
||||||
if stat, err := os.Stat(checkoutPaths.absolutePath); !os.IsNotExist(err) {
|
if stat, err := os.Stat(checkoutPaths.absolutePath); !os.IsNotExist(err) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
// No error stat'ing this path, indicating it's an existing checkout.
|
||||||
|
// Just retry another random token.
|
||||||
|
lastErr = ErrCheckoutAlreadyExists
|
||||||
if stat.IsDir() {
|
if stat.IsDir() {
|
||||||
logger.Debug().Msg("shaman: checkout path exists")
|
logger.Debug().Msg("shaman: checkout path exists")
|
||||||
} else {
|
} else {
|
||||||
logger.Error().Msg("shaman: checkout path exists but is not a directory")
|
logger.Warn().Msg("shaman: checkout path exists but is not a directory")
|
||||||
}
|
}
|
||||||
// No error stat'ing this path, indicating it's an existing checkout.
|
continue
|
||||||
return ResolvedCheckoutInfo{}, ErrCheckoutAlreadyExists
|
|
||||||
}
|
}
|
||||||
// If it's any other error, it's really a problem on our side.
|
// If it's any other error, it's really a problem on our side. Don't retry.
|
||||||
logger.Error().Err(err).Msg("shaman: unable to stat checkout directory")
|
logger.Error().Err(err).Msg("shaman: unable to stat checkout directory")
|
||||||
return ResolvedCheckoutInfo{}, err
|
return ResolvedCheckoutInfo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(checkoutPaths.absolutePath, 0777); err != nil {
|
if err := os.MkdirAll(checkoutPaths.absolutePath, 0777); err != nil {
|
||||||
logger.Error().Err(err).Msg("shaman: unable to create checkout directory")
|
lastErr = err
|
||||||
|
logger.Warn().Err(err).Msg("shaman: unable to create checkout directory")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Str("relPath", checkoutPaths.RelativePath).Msg("shaman: created checkout directory")
|
logger.Info().Str("relPath", checkoutPaths.RelativePath).Msg("shaman: created checkout directory")
|
||||||
return checkoutPaths, nil
|
return checkoutPaths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ResolvedCheckoutInfo{}, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
// EraseCheckout removes the checkout directory structure identified by the ID.
|
// EraseCheckout removes the checkout directory structure identified by the ID.
|
||||||
func (m *Manager) EraseCheckout(checkoutID string) error {
|
func (m *Manager) EraseCheckout(checkoutID string) error {
|
||||||
checkoutPaths, err := m.pathForCheckout(checkoutID)
|
checkoutPaths, err := m.pathForCheckout(checkoutID)
|
||||||
@ -231,3 +251,17 @@ func touchFile(blobPath string) error {
|
|||||||
logger.Debug().Msg("done touching")
|
logger.Debug().Msg("done touching")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// randomisedToken generates a random 4-character string.
|
||||||
|
// It is intended to add to a checkout path, to create some randomness and thus
|
||||||
|
// a higher chance of the path not yet existing.
|
||||||
|
func randomisedToken() string {
|
||||||
|
var runes = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
|
||||||
|
|
||||||
|
n := 4
|
||||||
|
s := make([]rune, n)
|
||||||
|
for i := range s {
|
||||||
|
s[i] = runes[rand.Intn(len(runes))]
|
||||||
|
}
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
@ -112,7 +112,7 @@ func (s *Server) IsEnabled() bool {
|
|||||||
|
|
||||||
// Checkout creates a directory, and symlinks the required files into it. The
|
// Checkout creates a directory, and symlinks the required files into it. The
|
||||||
// files must all have been uploaded to Shaman before calling this.
|
// files must all have been uploaded to Shaman before calling this.
|
||||||
func (s *Server) Checkout(ctx context.Context, checkout api.ShamanCheckout) error {
|
func (s *Server) Checkout(ctx context.Context, checkout api.ShamanCheckout) (string, error) {
|
||||||
return s.checkoutMan.Checkout(ctx, checkout)
|
return s.checkoutMan.Checkout(ctx, checkout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user