@@ -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