mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-02-08 16:53:49 +08:00
moved much remaining cloudtool logic to the server
This commit is contained in:
parent
59b48f03c6
commit
3d20759c36
12
.idea/dictionaries/ericf.xml
generated
12
.idea/dictionaries/ericf.xml
generated
@ -207,6 +207,8 @@
|
|||||||
<w>calced</w>
|
<w>calced</w>
|
||||||
<w>calcing</w>
|
<w>calcing</w>
|
||||||
<w>calcs</w>
|
<w>calcs</w>
|
||||||
|
<w>callargs</w>
|
||||||
|
<w>callname</w>
|
||||||
<w>callnode</w>
|
<w>callnode</w>
|
||||||
<w>cameraflash</w>
|
<w>cameraflash</w>
|
||||||
<w>camerashake</w>
|
<w>camerashake</w>
|
||||||
@ -292,6 +294,8 @@
|
|||||||
<w>compat</w>
|
<w>compat</w>
|
||||||
<w>compileall</w>
|
<w>compileall</w>
|
||||||
<w>compilelocations</w>
|
<w>compilelocations</w>
|
||||||
|
<w>completeargs</w>
|
||||||
|
<w>completecmd</w>
|
||||||
<w>compounddict</w>
|
<w>compounddict</w>
|
||||||
<w>compoundlist</w>
|
<w>compoundlist</w>
|
||||||
<w>configerror</w>
|
<w>configerror</w>
|
||||||
@ -619,6 +623,7 @@
|
|||||||
<w>ftxt</w>
|
<w>ftxt</w>
|
||||||
<w>fullclean</w>
|
<w>fullclean</w>
|
||||||
<w>fullcleanlist</w>
|
<w>fullcleanlist</w>
|
||||||
|
<w>fullfilepath</w>
|
||||||
<w>fullpath</w>
|
<w>fullpath</w>
|
||||||
<w>fullprice</w>
|
<w>fullprice</w>
|
||||||
<w>fullscreen</w>
|
<w>fullscreen</w>
|
||||||
@ -783,6 +788,7 @@
|
|||||||
<w>incrementbuild</w>
|
<w>incrementbuild</w>
|
||||||
<w>indentfilter</w>
|
<w>indentfilter</w>
|
||||||
<w>indentstr</w>
|
<w>indentstr</w>
|
||||||
|
<w>indexfile</w>
|
||||||
<w>indexfilename</w>
|
<w>indexfilename</w>
|
||||||
<w>indicies</w>
|
<w>indicies</w>
|
||||||
<w>indstr</w>
|
<w>indstr</w>
|
||||||
@ -919,6 +925,7 @@
|
|||||||
<w>lnumend</w>
|
<w>lnumend</w>
|
||||||
<w>lnumorig</w>
|
<w>lnumorig</w>
|
||||||
<w>lnums</w>
|
<w>lnums</w>
|
||||||
|
<w>loadpackage</w>
|
||||||
<w>localconfig</w>
|
<w>localconfig</w>
|
||||||
<w>locationgroup</w>
|
<w>locationgroup</w>
|
||||||
<w>locationgroups</w>
|
<w>locationgroups</w>
|
||||||
@ -1075,6 +1082,7 @@
|
|||||||
<w>newdbpath</w>
|
<w>newdbpath</w>
|
||||||
<w>newnode</w>
|
<w>newnode</w>
|
||||||
<w>newpath</w>
|
<w>newpath</w>
|
||||||
|
<w>nextcall</w>
|
||||||
<w>nextfilenum</w>
|
<w>nextfilenum</w>
|
||||||
<w>nextlevel</w>
|
<w>nextlevel</w>
|
||||||
<w>nfoo</w>
|
<w>nfoo</w>
|
||||||
@ -1148,6 +1156,8 @@
|
|||||||
<w>packagedir</w>
|
<w>packagedir</w>
|
||||||
<w>packagedirs</w>
|
<w>packagedirs</w>
|
||||||
<w>packagename</w>
|
<w>packagename</w>
|
||||||
|
<w>packagepath</w>
|
||||||
|
<w>packagepathstr</w>
|
||||||
<w>packageversion</w>
|
<w>packageversion</w>
|
||||||
<w>painttxtattr</w>
|
<w>painttxtattr</w>
|
||||||
<w>palmos</w>
|
<w>palmos</w>
|
||||||
@ -1757,6 +1767,8 @@
|
|||||||
<w>updatethencheck</w>
|
<w>updatethencheck</w>
|
||||||
<w>updatethencheckfast</w>
|
<w>updatethencheckfast</w>
|
||||||
<w>updatethencheckfull</w>
|
<w>updatethencheckfull</w>
|
||||||
|
<w>uploadargs</w>
|
||||||
|
<w>uploadcmd</w>
|
||||||
<w>uptime</w>
|
<w>uptime</w>
|
||||||
<w>useragent</w>
|
<w>useragent</w>
|
||||||
<w>useragentstring</w>
|
<w>useragentstring</w>
|
||||||
|
|||||||
391
tools/cloudtool
391
tools/cloudtool
@ -27,10 +27,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, asdict
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -38,7 +37,7 @@ import tempfile
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Optional, Dict, Any, Tuple, List, BinaryIO
|
from typing import Optional, Dict, Tuple, List, BinaryIO
|
||||||
|
|
||||||
# Version is sent to the master-server with all commands. Can be incremented
|
# Version is sent to the master-server with all commands. Can be incremented
|
||||||
# if we need to change behavior server-side to go along with client changes.
|
# if we need to change behavior server-side to go along with client changes.
|
||||||
@ -50,7 +49,6 @@ TOOL_NAME = 'cloudtool'
|
|||||||
MASTER_SERVER_ADDRESS = ('http://localhost:23524'
|
MASTER_SERVER_ADDRESS = ('http://localhost:23524'
|
||||||
if os.environ.get('CLOUDTOOL_LOCAL') == '1' else
|
if os.environ.get('CLOUDTOOL_LOCAL') == '1' else
|
||||||
'https://bamaster.appspot.com')
|
'https://bamaster.appspot.com')
|
||||||
USER_AGENT_STRING = 'cloudtool'
|
|
||||||
CACHE_DIR = Path('.cache/cloudtool')
|
CACHE_DIR = Path('.cache/cloudtool')
|
||||||
CACHE_DATA_PATH = Path(CACHE_DIR, 'state')
|
CACHE_DATA_PATH = Path(CACHE_DIR, 'state')
|
||||||
|
|
||||||
@ -60,17 +58,6 @@ CLRBLU = '\033[94m' # Glue.
|
|||||||
CLRRED = '\033[91m' # Red.
|
CLRRED = '\033[91m' # Red.
|
||||||
CLREND = '\033[0m' # End.
|
CLREND = '\033[0m' # End.
|
||||||
|
|
||||||
CMD_LOGIN = 'login'
|
|
||||||
CMD_LOGOUT = 'logout'
|
|
||||||
CMD_ASSETPACK = 'assetpack'
|
|
||||||
CMD_HELP = 'help'
|
|
||||||
|
|
||||||
# Note to self: keep this synced with server-side logic.
|
|
||||||
ASSET_PACKAGE_NAME_VALID_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_'
|
|
||||||
ASSET_PACKAGE_NAME_MAX_LENGTH = 32
|
|
||||||
ASSET_PATH_VALID_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_'
|
|
||||||
ASSET_PATH_MAX_LENGTH = 128
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class StateData:
|
class StateData:
|
||||||
@ -78,43 +65,39 @@ class StateData:
|
|||||||
login_token: Optional[str] = None
|
login_token: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
@dataclass
|
@dataclass
|
||||||
class Response:
|
class Response:
|
||||||
"""Response data from the master server for a command."""
|
"""Response sent from the cloudtool server to the client.
|
||||||
message: Optional[str]
|
|
||||||
error: Optional[str]
|
Attributes:
|
||||||
data: Any
|
message: If present, client should print this message.
|
||||||
|
error: If present, client should abort with this error message.
|
||||||
|
loadpackage: If present, client should load this package from a
|
||||||
|
location on disk (arg1) and push its manifest to a server command
|
||||||
|
(arg2) with provided args (arg3). The manifest should be added to
|
||||||
|
the args as 'manifest'. arg4 is the index file name whose
|
||||||
|
contents should be included with the manifest.
|
||||||
|
upload: If present, client should upload the requested files (arg1)
|
||||||
|
from the loaded package to a server command (arg2) with provided
|
||||||
|
args (arg3). Arg4 and arg5 are a server call and args which should
|
||||||
|
be called once all file uploads finish.
|
||||||
|
login: If present, a token that should be stored client-side and passed
|
||||||
|
with subsequent commands.
|
||||||
|
logout: If True, any existing client-side token should be discarded.
|
||||||
|
"""
|
||||||
|
message: Optional[str] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
loadpackage: Optional[Tuple[str, str, Dict, str]] = None
|
||||||
|
upload: Optional[Tuple[List[str], str, Dict, str, Dict]] = None
|
||||||
|
login: Optional[str] = None
|
||||||
|
logout: bool = False
|
||||||
|
|
||||||
|
|
||||||
class CleanError(Exception):
|
class CleanError(Exception):
|
||||||
"""Exception resulting in a clean error string print and exit."""
|
"""Exception resulting in a clean error string print and exit."""
|
||||||
|
|
||||||
|
|
||||||
class AssetType(Enum):
|
|
||||||
"""Types for asset files."""
|
|
||||||
TEXTURE = 'texture'
|
|
||||||
SOUND = 'sound'
|
|
||||||
DATA = 'data'
|
|
||||||
|
|
||||||
|
|
||||||
ASSET_SOURCE_FILE_EXTS = {
|
|
||||||
AssetType.TEXTURE: 'png',
|
|
||||||
AssetType.SOUND: 'wav',
|
|
||||||
AssetType.DATA: 'yaml',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Asset:
|
|
||||||
"""Data for a single asset."""
|
|
||||||
|
|
||||||
def __init__(self, package: AssetPackage, assettype: AssetType,
|
|
||||||
path: str) -> None:
|
|
||||||
self.assettype = assettype
|
|
||||||
self.path = path
|
|
||||||
self.filepath = os.path.join(
|
|
||||||
package.path, path + '.' + ASSET_SOURCE_FILE_EXTS[assettype])
|
|
||||||
|
|
||||||
|
|
||||||
def get_tz_offset_seconds() -> float:
|
def get_tz_offset_seconds() -> float:
|
||||||
"""Return the offset between utc and local time in seconds."""
|
"""Return the offset between utc and local time in seconds."""
|
||||||
import time
|
import time
|
||||||
@ -125,121 +108,60 @@ def get_tz_offset_seconds() -> float:
|
|||||||
return utc_offset
|
return utc_offset
|
||||||
|
|
||||||
|
|
||||||
# Note to self: keep this synced with server-side validation func...
|
@dataclass
|
||||||
def validate_asset_package_name(name: str) -> None:
|
class File:
|
||||||
"""Throw an exception on an invalid asset-package name."""
|
"""Represents a single file within a Package."""
|
||||||
if len(name) > ASSET_PACKAGE_NAME_MAX_LENGTH:
|
filehash: str
|
||||||
raise CleanError(f'Asset package name is too long: "{name}"')
|
filesize: int
|
||||||
if not name:
|
|
||||||
raise CleanError(f'Asset package name cannot be empty.')
|
|
||||||
if name[0] == '_' or name[-1] == '_':
|
|
||||||
raise CleanError(
|
|
||||||
f'Asset package name cannot start or end with underscore.')
|
|
||||||
if '__' in name:
|
|
||||||
raise CleanError(
|
|
||||||
f'Asset package name cannot contain sequential underscores.')
|
|
||||||
for char in name:
|
|
||||||
if char not in ASSET_PACKAGE_NAME_VALID_CHARS:
|
|
||||||
raise CleanError(
|
|
||||||
f'Found invalid char "{char}" in asset package name "{name}".')
|
|
||||||
|
|
||||||
|
|
||||||
# Note to self: keep this synced with server-side validation func...
|
class Package:
|
||||||
def validate_asset_path(path: str) -> None:
|
"""Represents a directory of files with some common purpose."""
|
||||||
"""Throw an exception on an invalid asset path."""
|
|
||||||
if len(path) > ASSET_PATH_MAX_LENGTH:
|
|
||||||
raise CleanError(f'Asset path is too long: "{path}"')
|
|
||||||
names = path.split('/')
|
|
||||||
for name in names:
|
|
||||||
if not name:
|
|
||||||
raise CleanError(f'Found empty component in asset path "{path}".')
|
|
||||||
for char in name:
|
|
||||||
if char not in ASSET_PATH_VALID_CHARS:
|
|
||||||
raise CleanError(
|
|
||||||
f'Found invalid char "{char}" in asset path "{path}".')
|
|
||||||
|
|
||||||
|
|
||||||
class AssetPackage:
|
|
||||||
"""Data for local or remote asset packages."""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.assets: Dict[str, Asset] = {}
|
|
||||||
self.path = Path('')
|
self.path = Path('')
|
||||||
self.name = 'untitled'
|
self.files: Dict[str, File] = {}
|
||||||
self.index = ''
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_from_disk(cls, path: Path) -> AssetPackage:
|
def load_from_disk(cls, path: Path) -> Package:
|
||||||
"""Load an asset package from files on disk."""
|
"""Create a package populated from a directory on disk."""
|
||||||
import yaml
|
package = Package()
|
||||||
indexfilename = 'assetpackage.yaml'
|
|
||||||
package = AssetPackage()
|
|
||||||
if not path.is_dir():
|
if not path.is_dir():
|
||||||
raise CleanError(f'Directory not found: "{path}"')
|
raise CleanError(f'Directory not found: "{path}"')
|
||||||
|
|
||||||
package.path = path
|
package.path = path
|
||||||
with open(Path(path, indexfilename)) as infile:
|
|
||||||
package.index = infile.read()
|
|
||||||
index = yaml.safe_load(package.index)
|
|
||||||
if not isinstance(index, dict):
|
|
||||||
raise CleanError(f'Root dict not found in {indexfilename}')
|
|
||||||
|
|
||||||
# Pull our name from the index file.
|
packagepathstr = str(path)
|
||||||
# (NOTE: can probably just let the server do this)
|
|
||||||
name = index.get('name')
|
|
||||||
if not isinstance(name, str):
|
|
||||||
raise CleanError(f'No "name" str found in {indexfilename}')
|
|
||||||
validate_asset_package_name(name)
|
|
||||||
package.name = name
|
|
||||||
|
|
||||||
# Build our list of Asset objs from the index.
|
paths: List[str] = []
|
||||||
assets = index.get('assets')
|
|
||||||
if not isinstance(assets, dict):
|
|
||||||
raise CleanError(f'No "assets" dict found in {indexfilename}')
|
|
||||||
for assetpath, assetdata in assets.items():
|
|
||||||
validate_asset_path(assetpath)
|
|
||||||
if not isinstance(assetdata, dict):
|
|
||||||
raise CleanError(
|
|
||||||
f'Invalid asset data for {assetpath} in {indexfilename}')
|
|
||||||
assettypestr = assetdata.get('type')
|
|
||||||
if not isinstance(assettypestr, str):
|
|
||||||
raise CleanError(
|
|
||||||
f'Invalid asset type for {assetpath} in {indexfilename}')
|
|
||||||
assettype = AssetType(assettypestr)
|
|
||||||
package.assets[assetpath] = Asset(package, assettype, assetpath)
|
|
||||||
|
|
||||||
return package
|
# Build the full list of package-relative paths.
|
||||||
|
for basename, _dirnames, filenames in os.walk(path):
|
||||||
|
for filename in filenames:
|
||||||
|
fullname = os.path.join(basename, filename)
|
||||||
|
assert fullname.startswith(packagepathstr)
|
||||||
|
paths.append(fullname[len(packagepathstr) + 1:])
|
||||||
|
|
||||||
def get_manifest(self) -> Dict:
|
|
||||||
"""Build a manifest of hashes and other info for the package."""
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from multiprocessing import cpu_count
|
from multiprocessing import cpu_count
|
||||||
|
|
||||||
manifest: Dict = {'name': self.name, 'files': {}, 'index': self.index}
|
def _get_file_info(filepath: str) -> Tuple[str, File]:
|
||||||
|
|
||||||
def _get_asset_info(iasset: Asset) -> Tuple[Asset, Dict]:
|
|
||||||
sha = hashlib.sha256()
|
sha = hashlib.sha256()
|
||||||
with open(iasset.filepath, 'rb') as infile:
|
fullfilepath = os.path.join(packagepathstr, filepath)
|
||||||
|
if not os.path.isfile(fullfilepath):
|
||||||
|
raise Exception(f'File not found: "{fullfilepath}"')
|
||||||
|
with open(fullfilepath, 'rb') as infile:
|
||||||
filebytes = infile.read()
|
filebytes = infile.read()
|
||||||
filesize = len(filebytes)
|
filesize = len(filebytes)
|
||||||
sha.update(filebytes)
|
sha.update(filebytes)
|
||||||
if not os.path.isfile(iasset.filepath):
|
return (filepath, File(filehash=sha.hexdigest(),
|
||||||
raise Exception(f'Asset file not found: "{iasset.filepath}"')
|
filesize=filesize))
|
||||||
info_out: Dict = {
|
|
||||||
'hash': sha.hexdigest(),
|
|
||||||
'size': filesize,
|
|
||||||
'ext': ASSET_SOURCE_FILE_EXTS[iasset.assettype]
|
|
||||||
}
|
|
||||||
return iasset, info_out
|
|
||||||
|
|
||||||
# Use all procs to hash files for extra speedy goodness.
|
# Now use all procs to hash the files efficiently.
|
||||||
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
|
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
|
||||||
for result in executor.map(_get_asset_info, self.assets.values()):
|
package.files = dict(executor.map(_get_file_info, paths))
|
||||||
asset, info = result
|
|
||||||
manifest['files'][asset.path] = info
|
|
||||||
|
|
||||||
return manifest
|
return package
|
||||||
|
|
||||||
|
|
||||||
class App:
|
class App:
|
||||||
@ -247,6 +169,7 @@ class App:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._state = StateData()
|
self._state = StateData()
|
||||||
|
self._package: Optional[Package] = None
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
"""Run the tool."""
|
"""Run the tool."""
|
||||||
@ -264,39 +187,29 @@ class App:
|
|||||||
raise CleanError('"make prereqs" check failed. '
|
raise CleanError('"make prereqs" check failed. '
|
||||||
'Install missing requirements and try again.')
|
'Install missing requirements and try again.')
|
||||||
|
|
||||||
self._load_cache()
|
self._load_state()
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print(f'{CLRRED}You must provide one or more arguments.{CLREND}')
|
print(f'{CLRRED}You must provide one or more arguments.{CLREND}')
|
||||||
self.do_misc_command(['help'])
|
self.run_command(['help'])
|
||||||
raise CleanError()
|
raise CleanError()
|
||||||
|
|
||||||
cmd = sys.argv[1]
|
# Simply pass all args to the server and let it do the thing.
|
||||||
if cmd == CMD_LOGIN:
|
self.run_command(sys.argv[1:])
|
||||||
self.do_login()
|
|
||||||
elif cmd == CMD_LOGOUT:
|
|
||||||
self.do_logout()
|
|
||||||
elif (cmd == CMD_ASSETPACK and len(sys.argv) > 2
|
|
||||||
and sys.argv[2] == 'put'):
|
|
||||||
self.do_assetpack_put()
|
|
||||||
else:
|
|
||||||
# For all other commands, simply pass them to the server verbatim.
|
|
||||||
self.do_misc_command(sys.argv[1:])
|
|
||||||
|
|
||||||
self._save_cache()
|
self._save_state()
|
||||||
|
|
||||||
def _load_cache(self) -> None:
|
def _load_state(self) -> None:
|
||||||
if not os.path.exists(CACHE_DATA_PATH):
|
if not os.path.exists(CACHE_DATA_PATH):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
with open(CACHE_DATA_PATH, 'r') as infile:
|
with open(CACHE_DATA_PATH, 'r') as infile:
|
||||||
self._state = StateData(**json.loads(infile.read()))
|
self._state = StateData(**json.loads(infile.read()))
|
||||||
except Exception:
|
except Exception:
|
||||||
print(CLRRED +
|
print(f'{CLRRED}Error loading {TOOL_NAME} data;'
|
||||||
f'Error loading {TOOL_NAME} data; resetting to defaults.' +
|
f' resetting to defaults.{CLREND}')
|
||||||
CLREND)
|
|
||||||
|
|
||||||
def _save_cache(self) -> None:
|
def _save_state(self) -> None:
|
||||||
if not CACHE_DIR.exists():
|
if not CACHE_DIR.exists():
|
||||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
with open(CACHE_DATA_PATH, 'w') as outfile:
|
with open(CACHE_DATA_PATH, 'w') as outfile:
|
||||||
@ -321,16 +234,16 @@ class App:
|
|||||||
response_raw_2.raise_for_status() # Except if anything went wrong.
|
response_raw_2.raise_for_status() # Except if anything went wrong.
|
||||||
assert isinstance(response_raw_2.content, bytes)
|
assert isinstance(response_raw_2.content, bytes)
|
||||||
output = json.loads(response_raw_2.content.decode())
|
output = json.loads(response_raw_2.content.decode())
|
||||||
assert isinstance(output, dict)
|
|
||||||
assert isinstance(output['m'], (str, type(None)))
|
|
||||||
assert isinstance(output['e'], (str, type(None)))
|
|
||||||
assert 'd' in output
|
|
||||||
response = Response(message=output['m'],
|
|
||||||
data=output['d'],
|
|
||||||
error=output['e'])
|
|
||||||
|
|
||||||
# Handle errors and print messages;
|
# Create a default Response and fill in only attrs we're aware of.
|
||||||
# (functionality common to all command types).
|
# (server may send attrs unknown to older clients)
|
||||||
|
response = Response()
|
||||||
|
for key, val in output.items():
|
||||||
|
if hasattr(response, key):
|
||||||
|
setattr(response, key, val)
|
||||||
|
|
||||||
|
# Handle common responses (can move these out of here at some point)
|
||||||
|
|
||||||
if response.error is not None:
|
if response.error is not None:
|
||||||
raise CleanError(response.error)
|
raise CleanError(response.error)
|
||||||
|
|
||||||
@ -339,93 +252,93 @@ class App:
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def do_login(self) -> None:
|
def _upload_file(self, filename: str, call: str, args: Dict) -> None:
|
||||||
"""Run the login command."""
|
print(f'{CLRBLU}Uploading {filename}{CLREND}', flush=True)
|
||||||
|
assert self._package is not None
|
||||||
|
with tempfile.TemporaryDirectory() as tempdir:
|
||||||
|
srcpath = Path(self._package.path, filename)
|
||||||
|
gzpath = Path(tempdir, 'file.gz')
|
||||||
|
subprocess.run(f'gzip --stdout "{srcpath}" > "{gzpath}"',
|
||||||
|
shell=True,
|
||||||
|
check=True)
|
||||||
|
with open(gzpath, 'rb') as infile:
|
||||||
|
putfiles: Dict = {'file': infile}
|
||||||
|
_response = self._servercmd(
|
||||||
|
call,
|
||||||
|
args,
|
||||||
|
files=putfiles,
|
||||||
|
)
|
||||||
|
|
||||||
if len(sys.argv) != 3:
|
def _handle_loadpackage_response(
|
||||||
raise CleanError('Expected a login code.')
|
self, response: Response) -> Optional[Tuple[str, Dict]]:
|
||||||
|
assert response.loadpackage is not None
|
||||||
|
assert len(response.loadpackage) == 4
|
||||||
|
(packagepath, callname, callargs, indexfile) = response.loadpackage
|
||||||
|
assert isinstance(packagepath, str)
|
||||||
|
assert isinstance(callname, str)
|
||||||
|
assert isinstance(callargs, dict)
|
||||||
|
assert isinstance(indexfile, str)
|
||||||
|
self._package = Package.load_from_disk(Path(packagepath))
|
||||||
|
|
||||||
login_code = sys.argv[2]
|
# Make the remote call they gave us with the package
|
||||||
response = self._servercmd('login', {'c': login_code})
|
# manifest added in.
|
||||||
|
with Path(self._package.path, indexfile).open() as infile:
|
||||||
|
index = infile.read()
|
||||||
|
callargs['manifest'] = {
|
||||||
|
'index': index,
|
||||||
|
'files': {
|
||||||
|
key: asdict(val)
|
||||||
|
for key, val in self._package.files.items()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return callname, callargs
|
||||||
|
|
||||||
# If the command returned cleanly, we should have a token we can use
|
def _handle_upload_response(
|
||||||
# to log in.
|
self, response: Response) -> Optional[Tuple[str, Dict]]:
|
||||||
token = response.data['logintoken']
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
assert isinstance(token, str)
|
assert response.upload is not None
|
||||||
|
assert self._package is not None
|
||||||
|
assert len(response.upload) == 5
|
||||||
|
(filenames, uploadcmd, uploadargs, completecmd,
|
||||||
|
completeargs) = response.upload
|
||||||
|
assert isinstance(filenames, list)
|
||||||
|
assert isinstance(uploadcmd, str)
|
||||||
|
assert isinstance(uploadargs, dict)
|
||||||
|
assert isinstance(completecmd, str)
|
||||||
|
assert isinstance(completeargs, dict)
|
||||||
|
|
||||||
aname = response.data['accountname']
|
def _do_filename(filename: str) -> None:
|
||||||
assert isinstance(aname, str)
|
self._upload_file(filename, uploadcmd, uploadargs)
|
||||||
|
|
||||||
print(f'{CLRGRN}Now logged in as {aname}.{CLREND}')
|
# Here we can run uploads concurrently if that goes faster...
|
||||||
self._state.login_token = token
|
# (should keep an eye on this to make sure its thread safe
|
||||||
|
# and behaves itself)
|
||||||
|
with ThreadPoolExecutor(max_workers=4) as executor:
|
||||||
|
# Convert the generator to a list to trigger any
|
||||||
|
# exceptions that occurred.
|
||||||
|
list(executor.map(_do_filename, filenames))
|
||||||
|
|
||||||
def do_logout(self) -> None:
|
# Lastly, run the 'upload complete' command we were passed.
|
||||||
"""Run the logout command."""
|
return completecmd, completeargs
|
||||||
self._state.login_token = None
|
|
||||||
print(f'{CLRGRN}Cloudtool is now logged out.{CLREND}')
|
|
||||||
|
|
||||||
def do_assetpack_put(self) -> None:
|
def run_command(self, args: List[str]) -> None:
|
||||||
"""Run an assetpackput command."""
|
"""Run a command to completion."""
|
||||||
|
|
||||||
if len(sys.argv) != 4:
|
nextcall: Optional[Tuple[str, Dict]] = ('toplevel', {'a': args})
|
||||||
raise CleanError('Expected a path to an assetpackage directory.')
|
|
||||||
|
|
||||||
path = Path(sys.argv[3])
|
# Now talk to the server in a loop until they are done with us.
|
||||||
package = AssetPackage.load_from_disk(path)
|
while nextcall is not None:
|
||||||
|
response = self._servercmd(*nextcall)
|
||||||
|
nextcall = None
|
||||||
|
|
||||||
# Send the server a manifest of everything we've got locally.
|
if response.loadpackage is not None:
|
||||||
manifest = package.get_manifest()
|
nextcall = self._handle_loadpackage_response(response)
|
||||||
response = self._servercmd('assetpackputmanifest', {'m': manifest})
|
if response.upload is not None:
|
||||||
|
nextcall = self._handle_upload_response(response)
|
||||||
# The server should give us a version and a set of files it wants.
|
if response.login is not None:
|
||||||
# Upload each of those.
|
self._state.login_token = response.login
|
||||||
upload_files: List[str] = response.data['upload_files']
|
if response.logout:
|
||||||
assert isinstance(upload_files, list)
|
self._state.login_token = None
|
||||||
assert all(isinstance(f, str) for f in upload_files)
|
|
||||||
version = response.data['package_version']
|
|
||||||
assert isinstance(version, str)
|
|
||||||
self._assetpack_put_upload(package, version, upload_files)
|
|
||||||
|
|
||||||
# Lastly, send a 'finish' command - this will prompt a response
|
|
||||||
# with info about the completed package.
|
|
||||||
_response = self._servercmd('assetpackputfinish', {
|
|
||||||
'packageversion': version,
|
|
||||||
})
|
|
||||||
|
|
||||||
def _assetpack_put_upload(self, package: AssetPackage, version: str,
|
|
||||||
files: List[str]) -> None:
|
|
||||||
|
|
||||||
# Upload the files one at a time.
|
|
||||||
# (we can potentially do this in parallel in the future).
|
|
||||||
for fnum, fname in enumerate(files):
|
|
||||||
print(
|
|
||||||
f'{CLRBLU}Uploading file {fnum+1} of {len(files)}: '
|
|
||||||
f'{fname}{CLREND}',
|
|
||||||
flush=True)
|
|
||||||
with tempfile.TemporaryDirectory() as tempdir:
|
|
||||||
asset = package.assets[fname]
|
|
||||||
srcpath = Path(asset.filepath)
|
|
||||||
gzpath = Path(tempdir, 'file.gz')
|
|
||||||
subprocess.run(f'gzip --stdout "{srcpath}" > "{gzpath}"',
|
|
||||||
shell=True,
|
|
||||||
check=True)
|
|
||||||
with open(gzpath, 'rb') as infile:
|
|
||||||
putfiles: Dict = {'file': infile}
|
|
||||||
_response = self._servercmd(
|
|
||||||
'assetpackputupload',
|
|
||||||
{
|
|
||||||
'packageversion': version,
|
|
||||||
'path': asset.path
|
|
||||||
},
|
|
||||||
files=putfiles,
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_misc_command(self, args: List[str]) -> None:
|
|
||||||
"""Run a miscellaneous command."""
|
|
||||||
|
|
||||||
# We don't do anything special with the response here; the normal
|
|
||||||
# error-handling/message-printing is all that happens.
|
|
||||||
self._servercmd('misc', {'a': args})
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -327,7 +327,7 @@ def gen_fulltest_buildfile_linux() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def resize_image() -> None:
|
def resize_image() -> None:
|
||||||
"""Resize an image and saves it to a new location.
|
"""Resize an image and save it to a new location.
|
||||||
|
|
||||||
args: xres, yres, src, dst
|
args: xres, yres, src, dst
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user