Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 46 additions & 9 deletions src/hosts/sd.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class SDHost(Host):

button = "Open SD card file"
settings_button = "SD card"
DELETE_FILE = -1

def __init__(self, path, sdpath=fpath("/sd")):
super().__init__(path)
Expand Down Expand Up @@ -69,20 +70,36 @@ def truncate(self, fname):
return fname[:18]+"..."+fname[-12:]

async def select_file(self, extensions):
while True:
files = self.list_files(extensions)
if len(files) == 0:
raise HostError("\n\nNo matching files found on the SD card\nAllowed: %s" % ", ".join(extensions))
# elif len(files) == 1:
# return self.sdpath+"/"+ files[0]

buttons = self.file_menu_buttons(files, extensions)
buttons += [(self.DELETE_FILE, "Delete file")]

fname = await self.manager.gui.menu(buttons, title="Select a file", last=(None, "Cancel"))
if fname == self.DELETE_FILE:
deleted = await self.delete_file_menu(files)
if deleted and len(self.list_files(extensions)) == 0:
return None
continue
return fname

def list_files(self, extensions):
files = sum([
[
f[0] for f in os.ilistdir(self.sdpath)
if f[0].lower().endswith(ext)
and f[1] == 0x8000
] for ext in extensions
], [])

if len(files) == 0:
raise HostError("\n\nNo matching files found on the SD card\nAllowed: %s" % ", ".join(extensions))
# elif len(files) == 1:
# return self.sdpath+"/"+ files[0]

files.sort()
return files

def file_menu_buttons(self, files, extensions):
buttons = []
for ext in extensions:
title = [(None, ext+" files")]
Expand All @@ -95,9 +112,29 @@ async def select_file(self, extensions):
buttons += [(None, "%s files - No files" % ext)]
else:
buttons += title + barr

fname = await self.manager.gui.menu(buttons, title="Select a file", last=(None, "Cancel"))
return fname
return buttons

async def delete_file_menu(self, files):
fname = await self.manager.gui.menu(
[(self.sdpath+"/"+f, self.truncate(f)) for f in files],
title="Delete SD card file",
last=(None, "Cancel")
)
if fname is None:
return False
shortname = fname.split("/")[-1]
confirm = await self.manager.gui.prompt(
"Delete file?",
"\n\nDelete %s from the SD card?\n\nThis cannot be undone." % shortname
)
if not confirm:
return False
try:
os.remove(fname)
except Exception:
raise HostError("Failed to delete file '%s'" % shortname)
await self.manager.gui.alert("Success!", "\n\n%s deleted." % shortname, button_text="OK")
return True

def completed_filename(self, filename):
suffix = "" if self.parent is None else ("."+hexlify(self.parent.fingerprint).decode())
Expand Down
10 changes: 10 additions & 0 deletions test/native_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ def decrypt(self, data):
if not hasattr(bcur, "bcur_decode_stream"):
bcur.bcur_decode_stream = lambda stream: stream

microur = _ensure_module("microur")
if not hasattr(microur, "__path__"):
microur.__path__ = []
_ensure_submodule("microur", "decoder", {
"FileURDecoder": type("FileURDecoder", (), {}),
})
_ensure_submodule("microur", "util", {
"cbor": types.SimpleNamespace(),
})

secp256k1 = _ensure_module("secp256k1")
if not hasattr(secp256k1, "EC_UNCOMPRESSED"):
secp256k1.EC_UNCOMPRESSED = 0
Expand Down
1 change: 1 addition & 0 deletions test/tests_native/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .test_wallet_manager_parsing import *
from .test_sd_host_file_menu import *
120 changes: 120 additions & 0 deletions test/tests_native/test_sd_host_file_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import asyncio
import gc
import os
import sys
import types
from unittest import TestCase

if sys.implementation.name != 'micropython':
from native_support import setup_native_stubs

setup_native_stubs()

import platform
from hosts.sd import SDHost
from tests.util import TEST_DIR, clear_testdir


class FakeGUI:
def __init__(self, menu_results=None, prompt_results=None):
self.menu_results = list(menu_results or [])
self.prompt_results = list(prompt_results or [])
self.menus = []
self.prompts = []
self.alerts = []

async def menu(self, buttons, title="", note=None, last=None):
self.menus.append({
"title": title,
"buttons": buttons,
"last": last,
})
return self.menu_results.pop(0)

async def prompt(self, title, msg, popup=False):
self.prompts.append((title, msg))
return self.prompt_results.pop(0)

async def alert(self, title, msg, button_text="OK", note=None):
self.alerts.append((title, msg, button_text))


class SDHostFileMenuTest(TestCase):
def setUp(self):
clear_testdir()
platform.maybe_mkdir(TEST_DIR)
platform.maybe_mkdir(TEST_DIR + "/host")
platform.maybe_mkdir(TEST_DIR + "/sd")
self.sdpath = TEST_DIR + "/sd"
self.host = SDHost(TEST_DIR + "/host", sdpath=self.sdpath)

def tearDown(self):
clear_testdir()
gc.collect()

def write_file(self, name, content=b"data"):
with open(self.sdpath + "/" + name, "wb") as f:
f.write(content)

def run_select_file(self, menu_results, prompt_results=None):
gui = FakeGUI(menu_results, prompt_results)
self.host.manager = types.SimpleNamespace(gui=gui)
result = asyncio.run(self.host.select_file([".psbt", ".txt", ".json"]))
return result, gui

def test_select_file_menu_includes_delete_action(self):
self.write_file("wallet.json")
result, gui = self.run_select_file([self.sdpath + "/wallet.json"])

self.assertEqual(result, self.sdpath + "/wallet.json")
self.assertIn((self.host.DELETE_FILE, "Delete file"), gui.menus[0]["buttons"])

def test_delete_file_removes_selected_file_after_confirmation(self):
self.write_file("wallet.json")
self.write_file("unsigned.psbt")

result, gui = self.run_select_file(
[
self.host.DELETE_FILE,
self.sdpath + "/wallet.json",
self.sdpath + "/unsigned.psbt",
],
[True],
)

self.assertEqual(result, self.sdpath + "/unsigned.psbt")
self.assertFalse(os.path.exists(self.sdpath + "/wallet.json"))
self.assertTrue(os.path.exists(self.sdpath + "/unsigned.psbt"))
self.assertEqual(gui.prompts[0][0], "Delete file?")
self.assertEqual(gui.alerts[0][0], "Success!")

def test_delete_file_cancel_keeps_file(self):
self.write_file("wallet.json")

result, gui = self.run_select_file(
[
self.host.DELETE_FILE,
self.sdpath + "/wallet.json",
self.sdpath + "/wallet.json",
],
[False],
)

self.assertEqual(result, self.sdpath + "/wallet.json")
self.assertTrue(os.path.exists(self.sdpath + "/wallet.json"))
self.assertEqual(gui.alerts, [])

def test_delete_last_matching_file_returns_none(self):
self.write_file("wallet.json")

result, gui = self.run_select_file(
[
self.host.DELETE_FILE,
self.sdpath + "/wallet.json",
],
[True],
)

self.assertIsNone(result)
self.assertFalse(os.path.exists(self.sdpath + "/wallet.json"))
self.assertEqual(gui.alerts[0][0], "Success!")
Loading