diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index dd783675..529499c9 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -459,6 +459,7 @@
dynload
eachother
easteregghunt
+ edcc
editcontroller
editgame
editorconfig
@@ -481,6 +482,7 @@
encerr
endcall
endindex
+ endmessage
endparen
endtime
ensurepip
@@ -1171,6 +1173,7 @@
passwd
patcomp
pathlib
+ pathnames
pathstonames
patsubst
pausable
diff --git a/tools/bacloud b/tools/bacloud
index 8425d1d7..88bd9b14 100755
--- a/tools/bacloud
+++ b/tools/bacloud
@@ -69,27 +69,37 @@ class Response:
"""Response sent from the bacloud server to the client.
Attributes:
- message: If present, client should print this message.
+ message: If present, client should print this message before any other
+ response processing (including error handling) occurs.
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
+ the args as 'manifest'. arg4, if present, is the index file name whose
contents should be included with the manifest.
- upload: If present, client should upload the requested files (arg1)
+ uploads: 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.
+ inline_downloads: If present, pathnames mapped to base64 gzipped data to
+ be written to the client. This should only be used for relatively
+ small files as they are all included inline as part of the response.
+ deletes: If present, file paths that should be deleted on the client.
+ endmessage: If present, a message that should be printed after all other
+ response processing is done.
"""
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
+ loadpackage: Optional[Tuple[str, str, Dict, Optional[str]]] = None
+ uploads: Optional[Tuple[List[str], str, Dict, str, Dict]] = None
login: Optional[str] = None
logout: bool = False
+ inline_downloads: Optional[Dict[str, str]] = None
+ deletes: Optional[List[str]] = None
+ endmessage: Optional[str] = None
class CleanError(Exception):
@@ -165,7 +175,7 @@ class App:
def __init__(self) -> None:
self._state = StateData()
- self._package: Optional[Package] = None
+ # self._package: Optional[Package] = None
self._project_root: Optional[Path] = None
def run(self) -> None:
@@ -243,16 +253,16 @@ class App:
output = json.loads(response_raw_2.content.decode())
# Create a default Response and fill in only attrs we're aware of.
- # (server may send attrs unknown to older clients)
+ # (newer server may send us attrs we're unaware of)
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)
-
+ # Handle a few things inline.
+ # (so this functionality is available even to recursive commands, etc.)
if response.message is not None:
- print(response.message)
+ print(response.message, flush=True)
if response.error is not None:
raise CleanError(response.error)
@@ -261,9 +271,10 @@ class App:
def _upload_file(self, filename: str, call: str, args: Dict) -> None:
print(f'{CLRBLU}Uploading {filename}{CLREND}', flush=True)
- assert self._package is not None
+ # assert self._package is not None
with tempfile.TemporaryDirectory() as tempdir:
- srcpath = Path(self._package.path, filename)
+ # srcpath = Path(self._package.path, filename)
+ srcpath = Path(filename)
gzpath = Path(tempdir, 'file.gz')
subprocess.run(f'gzip --stdout "{srcpath}" > "{gzpath}"',
shell=True,
@@ -284,30 +295,31 @@ class App:
assert isinstance(packagepath, str)
assert isinstance(callname, str)
assert isinstance(callargs, dict)
- assert isinstance(indexfile, str)
- self._package = Package.load_from_disk(Path(packagepath))
+ assert indexfile is None or isinstance(indexfile, str)
+ package = Package.load_from_disk(Path(packagepath))
# Make the remote call they gave us with the package
# manifest added in.
- with Path(self._package.path, indexfile).open() as infile:
- index = infile.read()
+ if indexfile is not None:
+ with Path(package.path, indexfile).open() as infile:
+ index = infile.read()
+ else:
+ index = ''
callargs['manifest'] = {
'index': index,
- 'files': {
- key: asdict(val)
- for key, val in self._package.files.items()
- }
+ 'files': {key: asdict(val)
+ for key, val in package.files.items()}
}
return callname, callargs
- def _handle_upload_response(
- self, response: Response) -> Optional[Tuple[str, Dict]]:
+ def _handle_uploads(self,
+ response: Response) -> Optional[Tuple[str, Dict]]:
from concurrent.futures import ThreadPoolExecutor
- assert response.upload is not None
- assert self._package is not None
- assert len(response.upload) == 5
+ assert response.uploads is not None
+ # assert self._package is not None
+ assert len(response.uploads) == 5
(filenames, uploadcmd, uploadargs, completecmd,
- completeargs) = response.upload
+ completeargs) = response.uploads
assert isinstance(filenames, list)
assert isinstance(uploadcmd, str)
assert isinstance(uploadargs, dict)
@@ -328,6 +340,23 @@ class App:
# Lastly, run the 'upload complete' command we were passed.
return completecmd, completeargs
+ def _handle_inline_downloads(self, response: Response) -> None:
+ """Handle inline file data to be saved to the client."""
+ import base64
+ import zlib
+ assert response.inline_downloads is not None
+ for fname, fdata in response.inline_downloads.items():
+ data_zipped = base64.b64decode(fdata)
+ data = zlib.decompress(data_zipped)
+ with open(fname, 'wb') as outfile:
+ outfile.write(data)
+
+ def _handle_deletes(self, response: Response) -> None:
+ """Handle file deletes."""
+ assert response.deletes is not None
+ for fname in response.deletes:
+ os.unlink(fname)
+
def run_user_command(self, args: List[str]) -> None:
"""Run a single user command to completion."""
@@ -339,12 +368,20 @@ class App:
nextcall = None
if response.loadpackage is not None:
nextcall = self._handle_loadpackage_response(response)
- if response.upload is not None:
- nextcall = self._handle_upload_response(response)
+ if response.uploads is not None:
+ nextcall = self._handle_uploads(response)
if response.login is not None:
self._state.login_token = response.login
if response.logout:
self._state.login_token = None
+ if response.inline_downloads:
+ self._handle_inline_downloads(response)
+ if response.deletes:
+ self._handle_deletes(response)
+
+ # This should always be printed last.
+ if response.endmessage is not None:
+ print(response.endmessage, flush=True)
if __name__ == '__main__':