Skip to content

Commit 38e2e1f

Browse files
committed
Sort the file list and simplify the tree buidling process
With a sorted list, new node can be created and updated at the same time. For example, when adding "/root/a/b/c" to the tree, node "a/" and node "b/" must exist. Just find the sibling list of node "b/" and add "c" to the list.
1 parent 0741370 commit 38e2e1f

File tree

1 file changed

+72
-38
lines changed

1 file changed

+72
-38
lines changed

mfr/extensions/zip/render.py

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,46 +27,90 @@ def render(self):
2727

2828
zip_file = ZipFile(self.file_path, 'r')
2929

30-
# ``ZipFile.filelist`` contains both files and folder. Using ``obj`` for better clarity.
31-
obj_list = self.sanitize_obj_list(zip_file.filelist)
32-
obj_tree = self.obj_list_to_tree(obj_list)
30+
# ``ZipFile.filelist`` contains both files and folders. Using ``obj`` for better clarity.
31+
sorted_obj_list = self.sanitize_obj_list(zip_file.filelist, sort=True)
32+
obj_tree = self.sorted_obj_list_to_tree(sorted_obj_list)
3333

3434
return self.TEMPLATE.render(data=obj_tree, base=self.assets_url)
3535

36-
def obj_list_to_tree(self, obj_list: list) -> List[dict]:
37-
"""Build the object tree from the object list. Each node is represented using a dictionary,
38-
where non-leaf nodes represent folders and leaves represent files. Return a list which
39-
contains only one element: the root node.
36+
def sorted_obj_list_to_tree(self, sorted_obj_list: list) -> List[dict]:
37+
"""Build the object tree from a sorted object list. Each node is a dictionary. Leaf nodes
38+
represent files and empty folders. Non-leaf nodes represent non-emtpy folders. Return a
39+
list of dictionary that contains only one element: the root node. The tree can be accessed
40+
and searched via the ``children`` key, of which the value is a list of child nodes.
4041
41-
:param obj_list: the object list
42+
:param sorted_obj_list: the sorted object list
4243
:rtype: ``List[dict]``
43-
:return: a list which contains only one element: the root node.
44+
:return: a list that contains only one element: the root node.
4445
"""
45-
4646
# Build the root node of the tree
4747
tree_root = {
4848
'text': self.metadata.name + self.metadata.ext,
4949
'icon': self.assets_url + '/img/file-ext-zip.png',
5050
'children': []
5151
}
52+
# Iterate through each path and build the tree
53+
for obj in sorted_obj_list:
54+
path_from_root = obj.filename
55+
print(path_from_root)
56+
path_segments = [segment for segment in path_from_root.split('/') if segment]
57+
# Ignore the root tree node
58+
if len(path_segments) == 1:
59+
continue
60+
# Find the parent node of the current object, always start from the root node
61+
parent = tree_root
62+
for index, segment in enumerate(path_segments):
63+
# the first segment is the tree node, skip
64+
if index == 0:
65+
continue
66+
# last segment is the current object, parents must have been found, end loop
67+
if index == len(path_segments) - 1:
68+
break
69+
# for a sorted list, every segment on the path must have a tree node
70+
sibling_list = parent.get('children', [])
71+
parent = self.find_node_among_siblings(segment, sibling_list)
72+
# TODO: do we need this assert?
73+
assert parent
74+
# Create a new node, update details and add it to the sibling list
75+
sibling_list = parent.get('children', [])
76+
is_folder = path_from_root[-1] == '/'
77+
new_node = {
78+
'text': path_segments[-1],
79+
'children': [],
80+
}
81+
self.update_node_with_attributes(new_node, obj, is_folder=is_folder)
82+
sibling_list.append(new_node)
83+
return [tree_root, ]
5284

53-
for obj in obj_list:
85+
# TODO: should we remove this function?
86+
def unsorted_obj_list_to_tree(self, obj_list: list) -> List[dict]:
87+
"""Build the object tree from an object list. Each node is a dictionary, where leaf nodes
88+
represent empty folders and files and non-leaf nodes represent non-emtpy folders. Return a
89+
list that contains only one element: the root node.
5490
91+
:param obj_list: the object list
92+
:rtype: ``List[dict]``
93+
:return: a list that contains only one element: the root node.
94+
"""
95+
# Build the root node of the tree
96+
tree_root = {
97+
'text': self.metadata.name + self.metadata.ext,
98+
'icon': self.assets_url + '/img/file-ext-zip.png',
99+
'children': []
100+
}
101+
for obj in obj_list:
55102
# For each object, always start from the root of the tree
56103
parent = tree_root
57104
path_from_root = obj.filename
58105
is_folder = path_from_root[-1] == '/'
59106
path_segments = [segment for segment in path_from_root.split('/') if segment]
60107
last_index = len(path_segments) - 1
61-
62108
# Iterate through the path segments list. Add the segment to tree if not already there
63109
# and update the details with the current object if it is the last one along the path.
64110
for index, segment in enumerate(path_segments):
65-
66111
# Check if the segment has already been added
67112
siblings = parent.get('children', [])
68113
current_node = self.find_node_among_siblings(segment, siblings)
69-
70114
# Found
71115
if current_node:
72116
if index == last_index:
@@ -78,7 +122,6 @@ def obj_list_to_tree(self, obj_list: list) -> List[dict]:
78122
# Otherwise, jump to the next segment with the current node as the new parent
79123
parent = current_node
80124
continue
81-
82125
# Not found
83126
new_node = {
84127
'text': segment,
@@ -90,13 +133,11 @@ def obj_list_to_tree(self, obj_list: list) -> List[dict]:
90133
self.update_node_with_attributes(new_node, obj, is_folder=is_folder)
91134
siblings.append(new_node)
92135
break
93-
94136
# Otherwise, append the new node to tree, jump to the next segment with the current
95137
# node as the new parent
96138
siblings.append(new_node)
97139
parent = new_node
98140
continue
99-
100141
return [tree_root, ]
101142

102143
def update_node_with_attributes(self, node: dict, obj: ZipInfo, is_folder: bool=True) -> None:
@@ -106,10 +147,8 @@ def update_node_with_attributes(self, node: dict, obj: ZipInfo, is_folder: bool=
106147
:param obj: the object that the node represents
107148
:param is_folder: the folder flag
108149
"""
109-
110150
date = '%d-%02d-%02d %02d:%02d:%02d' % obj.date_time[:6]
111151
size = sizeof_fmt(int(obj.file_size)) if obj.file_size else ''
112-
113152
if is_folder:
114153
icon_path = self.assets_url + '/img/folder.png'
115154
else:
@@ -118,7 +157,6 @@ def update_node_with_attributes(self, node: dict, obj: ZipInfo, is_folder: bool=
118157
icon_path = '{}/img/file-ext-{}.png'.format(self.assets_url, ext)
119158
else:
120159
icon_path = '{}/img/file-ext-generic.png'.format(self.assets_url)
121-
122160
node.update({
123161
'icon': icon_path,
124162
'data': {
@@ -132,11 +170,10 @@ def icon_exists(ext: str) -> bool:
132170
"""Check if an icon exists for the given file type. The extension string is converted to
133171
lower case.
134172
135-
:param ext: the file extension str
173+
:param ext: the file extension string
136174
:rtype: ``bool``
137-
:return: ``True`` if found; ``False`` otherwise
175+
:return: ``True`` if found, ``False`` otherwise
138176
"""
139-
140177
return os.path.isfile(os.path.join(
141178
os.path.dirname(__file__),
142179
'static',
@@ -145,44 +182,41 @@ def icon_exists(ext: str) -> bool:
145182
))
146183

147184
@staticmethod
148-
def sanitize_obj_list(obj_list: list) -> list:
149-
"""Remove macOS system and temporary files. Current implementation only removes '__MACOSX/'
150-
and '.DS_Store'. If necessary, extend the sanitizer to exclude more file types.
185+
def sanitize_obj_list(obj_list: list, sort: bool=False) -> list:
186+
"""Remove macOS system and temporary files with an option flag to sort the list. Current
187+
implementation only removes '__MACOSX/' and '.DS_Store'.
188+
189+
TODO: If necessary, extend the sanitizer to exclude more file types.
151190
152-
:param obj_list: a list of full paths for each file and folder in the zip
191+
:param obj_list: a list of full paths for each file or folder in the zip
192+
:param sort: the flag for returning a sorted list
153193
:rtype: ``list``
154194
:return: a sanitized list
155195
"""
156-
157196
sanitized_obj_list = []
158-
159197
for obj in obj_list:
160-
161198
obj_path = obj.filename
162199
# Ignore macOS '__MACOSX' folder for zip file
163200
if obj_path.startswith('__MACOSX/'):
164201
continue
165202
# Ignore macOS '.DS_STORE' file
166203
if obj_path == '.DS_Store' or obj_path.endswith('/.DS_Store'):
167204
continue
168-
169205
sanitized_obj_list.append(obj)
170-
206+
if sort:
207+
return sorted(sanitized_obj_list, key=lambda obj: obj.filename)
171208
return sanitized_obj_list
172209

173210
@staticmethod
174211
def find_node_among_siblings(segment: str, siblings: list) -> Union[dict, None]:
175-
"""Find if the folder or file represented by the path segment has already been added.
212+
"""Find the folder or file node represented by the path segment.
176213
177214
:param segment: the path segment
178-
:param siblings: the list containing all added sibling nodes
215+
:param siblings: the list containing all sibling nodes
179216
:rtype: ``Union[dict, None]``
180-
:return: the node if found or ``None`` otherwise
217+
:return: the node dictionary if found or ``None`` otherwise
181218
"""
182-
183219
for sibling in siblings:
184-
185220
if sibling.get('text', '') == segment:
186221
return sibling
187-
188222
return None

0 commit comments

Comments
 (0)