codexctl 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codexctl/__init__.py +462 -0
- codexctl/__main__.py +4 -0
- codexctl/analysis.py +30 -0
- codexctl/device.py +640 -0
- codexctl/server.py +172 -0
- codexctl/sync.py +218 -0
- codexctl/updates.py +368 -0
- codexctl-0.1.0.dist-info/LICENSE +674 -0
- codexctl-0.1.0.dist-info/METADATA +105 -0
- codexctl-0.1.0.dist-info/RECORD +11 -0
- codexctl-0.1.0.dist-info/WHEEL +4 -0
codexctl/server.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
import os
|
|
4
|
+
import hashlib
|
|
5
|
+
import binascii
|
|
6
|
+
|
|
7
|
+
response_ok = """<?xml version='1.0' encoding='UTF-8'?>
|
|
8
|
+
<response protocol="3.0" server="prod">
|
|
9
|
+
<daystart elapsed_seconds="42548" elapsed_days="5179"/>
|
|
10
|
+
<app status="ok" appid="{98DA7DF2-4E3E-4744-9DE6-EC931886ABAB}">
|
|
11
|
+
<event status="ok"/>
|
|
12
|
+
<ping status="ok"/>
|
|
13
|
+
</app>
|
|
14
|
+
</response>
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
response_template = """<?xml version="1.0" encoding="UTF-8"?>
|
|
18
|
+
<response protocol="3.0" server="prod">
|
|
19
|
+
<daystart elapsed_seconds="37145" elapsed_days="5179"/>
|
|
20
|
+
<app status="ok" appid="{{98DA7DF2-4E3E-4744-9DE6-EC931886ABAB}}">
|
|
21
|
+
<event status="ok"/>
|
|
22
|
+
<updatecheck status="ok">
|
|
23
|
+
<urls>
|
|
24
|
+
<url codebase="{codebase_url}"/>
|
|
25
|
+
</urls>
|
|
26
|
+
<manifest version="{version}">
|
|
27
|
+
<packages>
|
|
28
|
+
<package required="true" hash="{update_sha1}" name="{update_name}" size="{update_size}"/>
|
|
29
|
+
</packages>
|
|
30
|
+
<actions>
|
|
31
|
+
<action successsaction="default" sha256="{update_sha256}" event="postinstall" DisablePayloadBackoff="true"/>
|
|
32
|
+
</actions>
|
|
33
|
+
</manifest>
|
|
34
|
+
</updatecheck>
|
|
35
|
+
<ping status="ok"/>
|
|
36
|
+
</app>
|
|
37
|
+
</response>
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def getupdateinfo(platform, version, update_name):
|
|
42
|
+
full_path = os.path.join("updates", update_name)
|
|
43
|
+
|
|
44
|
+
update_size = str(os.path.getsize(full_path))
|
|
45
|
+
|
|
46
|
+
BUF_SIZE = 8192
|
|
47
|
+
|
|
48
|
+
sha1 = hashlib.sha1()
|
|
49
|
+
sha256 = hashlib.sha256()
|
|
50
|
+
with open(full_path, "rb") as f:
|
|
51
|
+
while True:
|
|
52
|
+
data = f.read(BUF_SIZE)
|
|
53
|
+
if not data:
|
|
54
|
+
break
|
|
55
|
+
sha1.update(data)
|
|
56
|
+
sha256.update(data)
|
|
57
|
+
update_sha1 = binascii.b2a_base64(sha1.digest(), newline=False).decode()
|
|
58
|
+
update_sha256 = binascii.b2a_base64(sha256.digest(), newline=False).decode()
|
|
59
|
+
return (update_sha1, update_sha256, update_size)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_available_version(version):
|
|
63
|
+
available_versions = scanUpdates()
|
|
64
|
+
|
|
65
|
+
for device, ids in available_versions.items():
|
|
66
|
+
if version in ids:
|
|
67
|
+
available_version = {device: ids}
|
|
68
|
+
|
|
69
|
+
return available_version
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def scanUpdates():
|
|
73
|
+
files = os.listdir("updates")
|
|
74
|
+
versions = {}
|
|
75
|
+
|
|
76
|
+
for f in files:
|
|
77
|
+
p = f.split("_")
|
|
78
|
+
if len(p) != 2:
|
|
79
|
+
continue
|
|
80
|
+
t = p[1].split(".")
|
|
81
|
+
if len(t) != 2:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
z = t[0].split("-")
|
|
85
|
+
|
|
86
|
+
version = p[0]
|
|
87
|
+
# print(version)
|
|
88
|
+
product = z[0]
|
|
89
|
+
|
|
90
|
+
if product not in versions or versions[product][0] < version:
|
|
91
|
+
versions[product] = (version, f)
|
|
92
|
+
|
|
93
|
+
return versions
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class MySimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
|
|
97
|
+
def do_POST(self):
|
|
98
|
+
length = int(self.headers.get("Content-Length"))
|
|
99
|
+
body = self.rfile.read(length).decode("utf-8")
|
|
100
|
+
# print(body)
|
|
101
|
+
print("Updating...")
|
|
102
|
+
xml = ET.fromstring(body)
|
|
103
|
+
updatecheck_node = xml.find("app/updatecheck")
|
|
104
|
+
|
|
105
|
+
# check for update
|
|
106
|
+
if updatecheck_node is not None:
|
|
107
|
+
version = xml.attrib["version"]
|
|
108
|
+
platform = xml.find("os").attrib["platform"]
|
|
109
|
+
print("requested: ", version)
|
|
110
|
+
print("platform: ", platform)
|
|
111
|
+
|
|
112
|
+
version, update_name = available_versions[platform]
|
|
113
|
+
|
|
114
|
+
update_sha1, update_sha256, update_size = getupdateinfo(
|
|
115
|
+
platform, version, update_name
|
|
116
|
+
)
|
|
117
|
+
params = {
|
|
118
|
+
"version": version,
|
|
119
|
+
"update_name": f"updates/{update_name}",
|
|
120
|
+
"update_sha1": update_sha1,
|
|
121
|
+
"update_sha256": update_sha256,
|
|
122
|
+
"update_size": update_size,
|
|
123
|
+
"codebase_url": host_url,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
response = response_template.format(**params)
|
|
127
|
+
# print("Response:")
|
|
128
|
+
# print(response)
|
|
129
|
+
self.send_response(200)
|
|
130
|
+
self.end_headers()
|
|
131
|
+
self.wfile.write(response.encode())
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
event_node = xml.find("app/event")
|
|
135
|
+
event_type = int(event_node.attrib["eventtype"])
|
|
136
|
+
event_result = int(event_node.attrib["eventresult"])
|
|
137
|
+
|
|
138
|
+
# post install status
|
|
139
|
+
if event_result != 0:
|
|
140
|
+
print("Update downloaded, please wait for device to install...")
|
|
141
|
+
if "errorcode" in event_node.attrib:
|
|
142
|
+
print("With errorcode:", event_node.attrib["errorcode"])
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# update done
|
|
146
|
+
if event_type == 14:
|
|
147
|
+
print("OK Response:")
|
|
148
|
+
print(response_ok)
|
|
149
|
+
self.send_response(200)
|
|
150
|
+
self.end_headers()
|
|
151
|
+
self.wfile.write(response_ok.encode())
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def startUpdate(versionsGiven, host, port=8080):
|
|
156
|
+
global available_versions
|
|
157
|
+
global host_url # I am aware globals are generally bad practice, but this is a quick and dirty solution
|
|
158
|
+
|
|
159
|
+
host_url = f"http://{host}:{port}/"
|
|
160
|
+
available_versions = versionsGiven
|
|
161
|
+
|
|
162
|
+
if not available_versions:
|
|
163
|
+
raise FileNotFoundError("Could not find any update files")
|
|
164
|
+
|
|
165
|
+
handler = MySimpleHTTPRequestHandler
|
|
166
|
+
print(f"Starting fake updater at {host}:{port}")
|
|
167
|
+
try:
|
|
168
|
+
httpd = HTTPServer((host, port), handler)
|
|
169
|
+
except OSError:
|
|
170
|
+
print("Error: Could not start fake updater. Is the port already in use?")
|
|
171
|
+
return
|
|
172
|
+
httpd.serve_forever()
|
codexctl/sync.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import os
|
|
3
|
+
import glob
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RmWebInterfaceAPI: # TODO: Add docstrings
|
|
9
|
+
def __init__(self, BASE="http://10.11.99.1/", logger=None):
|
|
10
|
+
self.logger = logger
|
|
11
|
+
|
|
12
|
+
if self.logger is None:
|
|
13
|
+
self.logger = logging
|
|
14
|
+
|
|
15
|
+
self.BASE = BASE
|
|
16
|
+
self.NAME_ATTRIBUTE = "VissibleName"
|
|
17
|
+
self.ID_ATTRIBUTE = "ID"
|
|
18
|
+
|
|
19
|
+
self.logger.debug(f"Base is: {BASE}")
|
|
20
|
+
|
|
21
|
+
def __POST(self, endpoint, data={}, fileUpload=False):
|
|
22
|
+
try:
|
|
23
|
+
logging.debug(
|
|
24
|
+
f"Sending POST request to {self.BASE + endpoint} with data {data}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if fileUpload:
|
|
28
|
+
result = requests.post(self.BASE + endpoint, files=data)
|
|
29
|
+
else:
|
|
30
|
+
result = requests.post(self.BASE + endpoint, data=data)
|
|
31
|
+
|
|
32
|
+
if result.status_code == 408:
|
|
33
|
+
self.logger.error("Request timed out!")
|
|
34
|
+
|
|
35
|
+
logging.debug(f"Result headers: {result.headers}")
|
|
36
|
+
if "application/json" in result.headers["Content-Type"]:
|
|
37
|
+
return result.json()
|
|
38
|
+
return result.content
|
|
39
|
+
|
|
40
|
+
except Exception:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def __get_documents_recursive(
|
|
44
|
+
self, folderId="", currentLocation="", currentDocuments=[]
|
|
45
|
+
):
|
|
46
|
+
data = self.__POST(f"documents/{folderId}")
|
|
47
|
+
|
|
48
|
+
for item in data:
|
|
49
|
+
self.logger.debug(f"Checking item: {item}")
|
|
50
|
+
|
|
51
|
+
if "fileType" in item:
|
|
52
|
+
item["location"] = currentLocation
|
|
53
|
+
currentDocuments.append(item)
|
|
54
|
+
else:
|
|
55
|
+
self.logger.debug(
|
|
56
|
+
f"Getting documents over {item[self.ID_ATTRIBUTE]}, current location is {currentLocation}/{item[self.NAME_ATTRIBUTE]}"
|
|
57
|
+
)
|
|
58
|
+
self.__get_documents_recursive(
|
|
59
|
+
item[self.ID_ATTRIBUTE],
|
|
60
|
+
f"{currentLocation}/{item[self.NAME_ATTRIBUTE]}",
|
|
61
|
+
currentDocuments,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return currentDocuments
|
|
65
|
+
|
|
66
|
+
def __get_folder_id(self, folderName, _from=""):
|
|
67
|
+
results = self.__POST(f"documents/{_from}")
|
|
68
|
+
|
|
69
|
+
if results is None:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
results.reverse() # We only want folders
|
|
73
|
+
|
|
74
|
+
for data in results:
|
|
75
|
+
self.logger.debug(f"Folder: {data}")
|
|
76
|
+
|
|
77
|
+
if "fileType" in data:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
if data[self.NAME_ATTRIBUTE].strip() == folderName.strip():
|
|
81
|
+
return data[self.ID_ATTRIBUTE]
|
|
82
|
+
|
|
83
|
+
self.logger.debug(
|
|
84
|
+
f"Getting folders over {folderName}, {data[self.ID_ATTRIBUTE]}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
recursiveResults = self.__get_folder_id(folderName, data[self.ID_ATTRIBUTE])
|
|
88
|
+
if recursiveResults is None:
|
|
89
|
+
continue
|
|
90
|
+
else:
|
|
91
|
+
return recursiveResults
|
|
92
|
+
|
|
93
|
+
def __get_docs(self, folderName="", recursive=True):
|
|
94
|
+
folderId = ""
|
|
95
|
+
|
|
96
|
+
if folderName:
|
|
97
|
+
folderId = self.__get_folder_id(folderName)
|
|
98
|
+
|
|
99
|
+
if folderId is None:
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
if recursive:
|
|
103
|
+
self.logger.debug(f"Calling recursive function on {folderName}")
|
|
104
|
+
return self.__get_documents_recursive(
|
|
105
|
+
folderId=folderId, currentLocation=folderName
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
data = self.__POST(f"documents/{folderId}")
|
|
109
|
+
|
|
110
|
+
for item in data:
|
|
111
|
+
item["location"] = ""
|
|
112
|
+
|
|
113
|
+
return [item for item in data if "fileType" in item]
|
|
114
|
+
|
|
115
|
+
def download(self, document, location="", overwrite=False):
|
|
116
|
+
filename = document[self.NAME_ATTRIBUTE]
|
|
117
|
+
if "/" in filename:
|
|
118
|
+
filename = filename.replace("/", "_")
|
|
119
|
+
|
|
120
|
+
self.logger.debug(f"Downloading {filename}, location {location}")
|
|
121
|
+
|
|
122
|
+
if not os.path.exists(location):
|
|
123
|
+
self.logger.debug("Download folder does not exist, creating it")
|
|
124
|
+
os.makedirs(location)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
fileLocation = f"{location}/{filename}.pdf"
|
|
128
|
+
|
|
129
|
+
if os.path.isfile(fileLocation) and overwrite is False:
|
|
130
|
+
self.logger.debug("Not overwriting file")
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
binaryData = self.__POST(
|
|
134
|
+
f"download/{document[self.ID_ATTRIBUTE]}/placeholder"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if isinstance(binaryData, dict):
|
|
138
|
+
print(f"Error trying to download {filename}: {binaryData}")
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
with open(fileLocation, "wb") as outFile:
|
|
142
|
+
outFile.write(binaryData)
|
|
143
|
+
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
except Exception as error:
|
|
147
|
+
print(f"Error trying to download {filename}: {error}")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def upload(self, input_paths, remoteFolder):
|
|
151
|
+
folderId = ""
|
|
152
|
+
if remoteFolder:
|
|
153
|
+
folderId = self.__get_folder_id(remoteFolder)
|
|
154
|
+
|
|
155
|
+
if folderId is None:
|
|
156
|
+
raise SystemExit(f"Error: Folder {remoteFolder} does not exist!")
|
|
157
|
+
|
|
158
|
+
self.__POST(f"documents/{folderId}") # Setting up for upload...
|
|
159
|
+
|
|
160
|
+
errors, documents = [], []
|
|
161
|
+
|
|
162
|
+
for document in input_paths: # This needs improvement...
|
|
163
|
+
if os.path.isdir(document):
|
|
164
|
+
for file in glob.glob(f"{document}/*"):
|
|
165
|
+
if not file.endswith(".pdf"):
|
|
166
|
+
self.logger.error(f"Error: {document} is not a pdf!")
|
|
167
|
+
else:
|
|
168
|
+
documents.append(file)
|
|
169
|
+
elif os.path.isfile(document):
|
|
170
|
+
if not document.endswith(".pdf"):
|
|
171
|
+
errors.append(document)
|
|
172
|
+
self.logger.error(f"Error: {document} is not a pdf!")
|
|
173
|
+
else:
|
|
174
|
+
documents.append(document)
|
|
175
|
+
else:
|
|
176
|
+
errors.append(document)
|
|
177
|
+
self.logger.error(f"Error: {document} is not a file or directory!")
|
|
178
|
+
|
|
179
|
+
for document in documents:
|
|
180
|
+
self.logger.debug(
|
|
181
|
+
f"Uploading {document} to {remoteFolder if remoteFolder else 'root'}"
|
|
182
|
+
)
|
|
183
|
+
with open(document, "rb") as inFile:
|
|
184
|
+
response = self.__POST("upload", data={"file": inFile}, fileUpload=True)
|
|
185
|
+
|
|
186
|
+
if response is None:
|
|
187
|
+
self.logger.error(
|
|
188
|
+
f"Error: Unknown error while uploading {document}!"
|
|
189
|
+
)
|
|
190
|
+
errors.append(document)
|
|
191
|
+
elif response == {"status": "Upload successful"}:
|
|
192
|
+
self.logger.debug(f"Uploaded {document} successfully!")
|
|
193
|
+
|
|
194
|
+
if len(errors) > 0:
|
|
195
|
+
print("The following files failed to upload: " + ",".join(errors))
|
|
196
|
+
|
|
197
|
+
print(f"Done! {len(documents)-len(errors)} files were uploaded.")
|
|
198
|
+
|
|
199
|
+
def sync(self, localFolder, remoteFolder="", overwrite=False, recursive=True):
|
|
200
|
+
count = 0
|
|
201
|
+
|
|
202
|
+
if not os.path.exists(localFolder):
|
|
203
|
+
self.logger.debug("Local folder does not exist, creating it")
|
|
204
|
+
os.mkdir(localFolder)
|
|
205
|
+
|
|
206
|
+
documents = self.__get_docs(remoteFolder, recursive)
|
|
207
|
+
|
|
208
|
+
if documents == {}:
|
|
209
|
+
print("No documents were found!")
|
|
210
|
+
|
|
211
|
+
else:
|
|
212
|
+
for doc in documents:
|
|
213
|
+
self.logger.debug(f"Processing {doc}")
|
|
214
|
+
count += 1
|
|
215
|
+
self.download(
|
|
216
|
+
doc, f"{localFolder}/{doc['location']}", overwrite=overwrite
|
|
217
|
+
)
|
|
218
|
+
print(f"Done! {count} files were exported.")
|