Skip to content

Commit 5785bc5

Browse files
AppManager: support .mpk/.zip files with compression and an unnecessary top-level directory
1 parent 4c6dc2a commit 5785bc5

6 files changed

Lines changed: 184 additions & 2 deletions

File tree

internal_filesystem/lib/mpos/content/app_manager.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,15 +178,75 @@ def install_mpk(temp_zip_path, dest_folder):
178178

179179
# Step 2: Unzip the file
180180
print("Unzipping it to:", dest_folder)
181+
181182
with zipfile.ZipFile(temp_zip_path, "r") as zip_ref:
182-
zip_ref.extractall(dest_folder)
183+
dest_name = dest_folder.rstrip(os.sep).split(os.sep)[-1]
184+
nested_prefix = f"{dest_name}/"
185+
top_dirs = set()
186+
has_top_files = False
187+
for member in zip_ref.infolist():
188+
name = member.filename.lstrip("/")
189+
if not name:
190+
continue
191+
stripped = name.strip("/")
192+
if not stripped:
193+
continue
194+
if "/" in stripped:
195+
top_dirs.add(stripped.split("/", 1)[0])
196+
else:
197+
if not member.is_dir():
198+
has_top_files = True
199+
else:
200+
top_dirs.add(stripped)
201+
202+
strip_prefix = ""
203+
if not has_top_files and len(top_dirs) == 1:
204+
sole_top = next(iter(top_dirs))
205+
if sole_top == dest_name:
206+
strip_prefix = nested_prefix
207+
else:
208+
raise ValueError(
209+
"Invalid top-level dir '{}' (expected '{}')".format(
210+
sole_top,
211+
dest_name,
212+
)
213+
)
214+
215+
for member in zip_ref.infolist():
216+
arcname = member.filename.lstrip("/")
217+
if strip_prefix:
218+
if not arcname.startswith(strip_prefix):
219+
continue
220+
arcname = arcname[len(strip_prefix):]
221+
if not arcname:
222+
continue
223+
original_name = member.filename
224+
try:
225+
member.filename = arcname
226+
zip_ref.extract(member, dest_folder)
227+
finally:
228+
member.filename = original_name
183229
print("Unzipped successfully")
184230
# Step 3: Clean up
185231
os.remove(temp_zip_path)
186232
print("Removed temporary .mpk file")
187233
except Exception as e:
188-
print(f"Unzip and cleanup failed: {e}")
234+
print(f"install_mpk got exception, will attempt cleanup: {e}")
189235
# Would be good to show error message here if it fails...
236+
try:
237+
import shutil
238+
shutil.rmtree(dest_folder)
239+
except Exception as e:
240+
#print(f"install_mpk got shutil.rmtree exception: {e}")
241+
#import sys
242+
#sys.print_exception(e)
243+
pass
244+
try:
245+
os.remove(temp_zip_path)
246+
except Exception as e:
247+
print(f"install_mpk got os.remove exception: {e}")
248+
import sys
249+
sys.print_exception(e)
190250
AppManager.refresh_apps()
191251

192252
@staticmethod
6.74 KB
Binary file not shown.
7.18 KB
Binary file not shown.
6.82 KB
Binary file not shown.
7.35 KB
Binary file not shown.

tests/test_app_manager.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import os
2+
import shutil
3+
import unittest
4+
5+
from mpos import AppManager
6+
7+
class TestAppManagerInstallMpk(unittest.TestCase):
8+
APP_FULLNAME = "com.micropythonos.ziptest"
9+
APP_ROOT = f"apps/{APP_FULLNAME}"
10+
TEMP_MPK = "data/tmp_ziptest_Xr0.mpk"
11+
12+
def setUp(self):
13+
self.dest_folder = self.APP_ROOT
14+
self.temp_mpk = self.TEMP_MPK
15+
self._remove_path(self.dest_folder)
16+
self._remove_path(self.temp_mpk)
17+
try:
18+
os.stat("data")
19+
except OSError:
20+
os.mkdir("data")
21+
22+
def tearDown(self):
23+
self._remove_path(self.dest_folder)
24+
self._remove_path(self.temp_mpk)
25+
26+
def _remove_path(self, path):
27+
try:
28+
st = os.stat(path)
29+
except OSError:
30+
return
31+
if st[0] & 0x4000:
32+
shutil.rmtree(path)
33+
else:
34+
os.remove(path)
35+
36+
def _copy_file(self, source, dest):
37+
with open(source, "rb") as source_file:
38+
with open(dest, "wb") as dest_file:
39+
while True:
40+
chunk = source_file.read(1024)
41+
if not chunk:
42+
break
43+
dest_file.write(chunk)
44+
45+
def _exists(self, path):
46+
try:
47+
os.stat(path)
48+
return True
49+
except OSError:
50+
return False
51+
52+
def _assert_dir(self, path):
53+
st = os.stat(path)
54+
self.assertTrue(st[0] & 0x4000)
55+
56+
def _assert_file_size(self, path, expected_size):
57+
st = os.stat(path)
58+
self.assertTrue(st[0] & 0x8000)
59+
self.assertEqual(st[6], expected_size)
60+
61+
def _assert_app_tree(self, root):
62+
self._assert_dir(root)
63+
self._assert_dir(f"{root}/assets")
64+
self._assert_dir(f"{root}/META-INF")
65+
self._assert_dir(f"{root}/res")
66+
self._assert_dir(f"{root}/res/mipmap-mdpi")
67+
68+
self._assert_file_size(
69+
f"{root}/assets/hello.py",
70+
232,
71+
)
72+
self._assert_file_size(
73+
f"{root}/META-INF/MANIFEST.JSON",
74+
406,
75+
)
76+
self._assert_file_size(
77+
f"{root}/res/mipmap-mdpi/icon_64x64.png",
78+
5499,
79+
)
80+
81+
def test_install_mpk_extracts_files(self):
82+
# Uncompressed and without extended attributes:
83+
source_mpk = "../tests/com.micropythonos.ziptest_Xr0.mpk"
84+
self._copy_file(source_mpk, self.temp_mpk)
85+
86+
AppManager.install_mpk(self.temp_mpk, self.dest_folder)
87+
88+
self.assertFalse(self._exists(self.temp_mpk))
89+
self._assert_app_tree(self.APP_ROOT)
90+
91+
def test_install_mpk_extracts_files_xr(self):
92+
# Default zip (deflate.RAW)
93+
source_mpk = "../tests/com.micropythonos.ziptest_r.mpk"
94+
self._copy_file(source_mpk, self.temp_mpk)
95+
96+
AppManager.install_mpk(self.temp_mpk, self.dest_folder)
97+
98+
self.assertFalse(self._exists(self.temp_mpk))
99+
self._assert_app_tree(self.APP_ROOT)
100+
101+
102+
def test_install_mpk_extracts_files_topdir(self):
103+
# Zip contains top dir
104+
source_mpk = "../tests/com.micropythonos.ziptest_topdir.mpk"
105+
self._copy_file(source_mpk, self.temp_mpk)
106+
107+
self.dest_folder = "apps/com.micropythonos.ziptest"
108+
AppManager.install_mpk(self.temp_mpk, self.dest_folder)
109+
110+
self.assertFalse(self._exists(self.temp_mpk))
111+
self._assert_app_tree(self.dest_folder)
112+
113+
def test_install_mpk_rejects_invalid_topdir(self):
114+
# Zip contains top dir that does not match destination name
115+
source_mpk = "../tests/com.micropythonos.ziptest_invalid_topdir.mpk"
116+
self._copy_file(source_mpk, self.temp_mpk)
117+
118+
self.dest_folder = "apps/com.micropythonos.ziptest_invalid"
119+
AppManager.install_mpk(self.temp_mpk, self.dest_folder)
120+
121+
self.assertFalse(self._exists(self.temp_mpk))
122+
self.assertFalse(self._exists(self.dest_folder))

0 commit comments

Comments
 (0)