more bacloud work

This commit is contained in:
Eric Froemling 2020-01-18 18:24:38 -08:00
parent 1f7f5e49f5
commit 414a8e776b

View File

@ -29,7 +29,7 @@ import sys
import os import os
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dataclasses import dataclass, asdict from dataclasses import dataclass
import json import json
import subprocess import subprocess
import tempfile import tempfile
@ -72,34 +72,36 @@ class Response:
message: If present, client should print this message before any other message: If present, client should print this message before any other
response processing (including error handling) occurs. response processing (including error handling) occurs.
error: If present, client should abort with this error 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, if present, is the index file name whose
contents should be included with the manifest.
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 login: If present, a token that should be stored client-side and passed
with subsequent commands. with subsequent commands.
logout: If True, any existing client-side token should be discarded. logout: If True, any existing client-side token should be discarded.
inline_downloads: If present, pathnames mapped to base64 gzipped data to dirmanifest: If present, client should generate a manifest of this dir.
It should be added to endcommand args as 'manifest'.
uploads: If present, client should upload the requested files (arg1)
individually to a server command (arg2) with provided args (arg3).
uploads_inline: If present, a list of pathnames that should be base64
gzipped and uploaded to an 'uploads_inline' dict in endcommand args.
This should be limited to relatively small files.
downloads_inline: If present, pathnames mapped to base64 gzipped data to
be written to the client. This should only be used for relatively 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. 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. deletes: If present, file paths that should be deleted on the client.
endmessage: If present, a message that should be printed after all other endmessage: If present, a message that should be printed after all other
response processing is done. response processing is done.
endcommand: If present, this command is run with these args at the end
of response processing.
""" """
message: Optional[str] = None message: Optional[str] = None
error: Optional[str] = None error: Optional[str] = None
loadpackage: Optional[Tuple[str, str, Dict, Optional[str]]] = None
uploads: Optional[Tuple[List[str], str, Dict, str, Dict]] = None
login: Optional[str] = None login: Optional[str] = None
logout: bool = False logout: bool = False
inline_downloads: Optional[Dict[str, str]] = None dirmanifest: Optional[str] = None
uploads: Optional[Tuple[List[str], str, Dict]] = None
uploads_inline: Optional[List[str]] = None
downloads_inline: Optional[Dict[str, str]] = None
deletes: Optional[List[str]] = None deletes: Optional[List[str]] = None
endmessage: Optional[str] = None endmessage: Optional[str] = None
endcommand: Optional[Tuple[str, Dict]] = None
class CleanError(Exception): class CleanError(Exception):
@ -175,8 +177,8 @@ class App:
def __init__(self) -> None: def __init__(self) -> None:
self._state = StateData() self._state = StateData()
# self._package: Optional[Package] = None
self._project_root: Optional[Path] = None self._project_root: Optional[Path] = None
self._end_command_args: Dict = {}
def run(self) -> None: def run(self) -> None:
"""Run the tool.""" """Run the tool."""
@ -271,9 +273,7 @@ class App:
def _upload_file(self, filename: str, call: str, args: Dict) -> None: def _upload_file(self, filename: str, call: str, args: Dict) -> None:
print(f'{CLRBLU}Uploading {filename}{CLREND}', flush=True) print(f'{CLRBLU}Uploading {filename}{CLREND}', flush=True)
# assert self._package is not None
with tempfile.TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory() as tempdir:
# srcpath = Path(self._package.path, filename)
srcpath = Path(filename) srcpath = Path(filename)
gzpath = Path(tempdir, 'file.gz') gzpath = Path(tempdir, 'file.gz')
subprocess.run(f'gzip --stdout "{srcpath}" > "{gzpath}"', subprocess.run(f'gzip --stdout "{srcpath}" > "{gzpath}"',
@ -287,44 +287,48 @@ class App:
files=putfiles, files=putfiles,
) )
def _handle_loadpackage_response( def _handle_dirmanifest_response(self, response: Response) -> None:
self, response: Response) -> Optional[Tuple[str, Dict]]: from dataclasses import asdict
assert response.loadpackage is not None assert response.dirmanifest is not None
assert len(response.loadpackage) == 4 # assert len(response.dirmanifest) == 2
(packagepath, callname, callargs, indexfile) = response.loadpackage # (packagepath, indexfile) = response.dirmanifest
assert isinstance(packagepath, str) assert isinstance(response.dirmanifest, str)
assert isinstance(callname, str) # assert isinstance(callname, str)
assert isinstance(callargs, dict) # assert isinstance(callargs, dict)
assert indexfile is None or isinstance(indexfile, str) # assert indexfile is None or isinstance(indexfile, str)
package = Package.load_from_disk(Path(packagepath)) package = Package.load_from_disk(Path(response.dirmanifest))
# Make the remote call they gave us with the package # Make the remote call they gave us with the package
# manifest added in. # manifest added in.
if indexfile is not None: # if indexfile is not None:
with Path(package.path, indexfile).open() as infile: # with Path(package.path, indexfile).open() as infile:
index = infile.read() # index = infile.read()
else: # else:
index = '' # index = ''
callargs['manifest'] = { # callargs['manifest'] = {
'index': index, # 'index': index,
# 'files': {key: asdict(val)
# for key, val in package.files.items()}
# }
# Store the manifest to be included with our next called command.
self._end_command_args['manifest'] = {
'files': {key: asdict(val) 'files': {key: asdict(val)
for key, val in package.files.items()} for key, val in package.files.items()}
} }
return callname, callargs # return callname, callargs
def _handle_uploads(self, def _handle_uploads(self, response: Response) -> None:
response: Response) -> Optional[Tuple[str, Dict]]:
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
assert response.uploads is not None assert response.uploads is not None
# assert self._package is not None assert len(response.uploads) == 3
assert len(response.uploads) == 5 filenames, uploadcmd, uploadargs = response.uploads
(filenames, uploadcmd, uploadargs, completecmd,
completeargs) = response.uploads
assert isinstance(filenames, list) assert isinstance(filenames, list)
assert isinstance(uploadcmd, str) assert isinstance(uploadcmd, str)
assert isinstance(uploadargs, dict) assert isinstance(uploadargs, dict)
assert isinstance(completecmd, str)
assert isinstance(completeargs, dict) # assert isinstance(completecmd, str)
# assert isinstance(completeargs, dict)
def _do_filename(filename: str) -> None: def _do_filename(filename: str) -> None:
self._upload_file(filename, uploadcmd, uploadargs) self._upload_file(filename, uploadcmd, uploadargs)
@ -338,14 +342,14 @@ class App:
list(executor.map(_do_filename, filenames)) list(executor.map(_do_filename, filenames))
# Lastly, run the 'upload complete' command we were passed. # Lastly, run the 'upload complete' command we were passed.
return completecmd, completeargs # return completecmd, completeargs
def _handle_inline_downloads(self, response: Response) -> None: def _handle_downloads_inline(self, response: Response) -> None:
"""Handle inline file data to be saved to the client.""" """Handle inline file data to be saved to the client."""
import base64 import base64
import zlib import zlib
assert response.inline_downloads is not None assert response.downloads_inline is not None
for fname, fdata in response.inline_downloads.items(): for fname, fdata in response.downloads_inline.items():
data_zipped = base64.b64decode(fdata) data_zipped = base64.b64decode(fdata)
data = zlib.decompress(data_zipped) data = zlib.decompress(data_zipped)
with open(fname, 'wb') as outfile: with open(fname, 'wb') as outfile:
@ -357,6 +361,22 @@ class App:
for fname in response.deletes: for fname in response.deletes:
os.unlink(fname) os.unlink(fname)
def _handle_uploads_inline(self, response: Response) -> None:
"""Handle uploading files inline."""
import base64
import zlib
assert response.uploads_inline is not None
files: Dict[str, str] = {}
for filepath in response.uploads_inline:
if not os.path.exists(filepath):
raise CleanError(f'File not found: {filepath}')
with open(filepath, 'rb') as infile:
data = infile.read()
data_zipped = zlib.compress(data)
data_base64 = base64.b64encode(data_zipped).decode()
files[filepath] = data_base64
self._end_command_args['uploads_inline'] = files
def run_user_command(self, args: List[str]) -> None: def run_user_command(self, args: List[str]) -> None:
"""Run a single user command to completion.""" """Run a single user command to completion."""
@ -364,24 +384,29 @@ class App:
# Now talk to the server in a loop until there's nothing left to do. # Now talk to the server in a loop until there's nothing left to do.
while nextcall is not None: while nextcall is not None:
self._end_command_args = {}
response = self._servercmd(*nextcall) response = self._servercmd(*nextcall)
nextcall = None nextcall = None
if response.loadpackage is not None:
nextcall = self._handle_loadpackage_response(response)
if response.uploads is not None:
nextcall = self._handle_uploads(response)
if response.login is not None: if response.login is not None:
self._state.login_token = response.login self._state.login_token = response.login
if response.logout: if response.logout:
self._state.login_token = None self._state.login_token = None
if response.inline_downloads: if response.dirmanifest is not None:
self._handle_inline_downloads(response) self._handle_dirmanifest_response(response)
if response.uploads_inline is not None:
self._handle_uploads_inline(response)
if response.uploads is not None:
self._handle_uploads(response)
if response.downloads_inline:
self._handle_downloads_inline(response)
if response.deletes: if response.deletes:
self._handle_deletes(response) self._handle_deletes(response)
# This should always be printed last.
if response.endmessage is not None: if response.endmessage is not None:
print(response.endmessage, flush=True) print(response.endmessage, flush=True)
if response.endcommand is not None:
nextcall = response.endcommand
for key, val in self._end_command_args.items():
nextcall[1][key] = val
if __name__ == '__main__': if __name__ == '__main__':