1
1
import os
2
- from typing import List
3
- from zipfile import ZipFile
2
+ from typing import List , Union
3
+ from zipfile import ZipFile , ZipInfo
4
4
5
5
from mako .lookup import TemplateLookup
6
6
@@ -27,82 +27,108 @@ def render(self):
27
27
28
28
zip_file = ZipFile (self .file_path , 'r' )
29
29
30
- file_list = self .sanitize_file_list (zip_file .filelist )
31
- file_tree = self .file_list_to_tree (file_list )
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 )
32
33
33
- return self .TEMPLATE .render (data = file_tree , base = self .assets_url )
34
+ return self .TEMPLATE .render (data = obj_tree , base = self .assets_url )
34
35
35
- def file_list_to_tree (self , file_list : list ) -> List [dict ]:
36
- """Build the file tree and return a "tree".
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.
37
40
38
- TODO: Fix this algorithm
39
- This algorithm only works when the ``file_list`` are in strict alphabetical order. Here is
40
- an example file A.zip where list 1 fails while list 2 succeed.
41
-
42
- A.zip
43
- --- A/
44
- --- A/aa.png
45
- --- B/ab.png
46
-
47
- File list 1: [ A/, A/B/, A/A/, A/A/aa.png, A/B/ab.png, ]
48
-
49
- File list 2: [ A/, A/A/, A/A/aa.png, A/B/, A/B/ab.png, ]
50
-
51
- :param file_list: the sanitized file list
41
+ :param obj_list: the object list
52
42
:rtype: ``List[dict]``
53
- :return: a "tree" in form of a list which contains one dictionary as the root node
43
+ :return: a list which contains only one element: the root node.
54
44
"""
55
45
56
- icons_url = self .assets_url + '/img'
57
-
58
- # Build the root of the file tree
59
- tree_root = [{
46
+ # Build the root node of the tree
47
+ tree_root = {
60
48
'text' : self .metadata .name + self .metadata .ext ,
61
- 'icon' : icons_url + '/file-ext-zip.png' ,
49
+ 'icon' : self . assets_url + '/img /file-ext-zip.png' ,
62
50
'children' : []
63
- }]
64
-
65
- # Iteratively build the file tree for each file and folder.egments.
66
- for file in file_list :
67
-
68
- node_path = tree_root [0 ]
69
-
70
- # Split the full path into segments, add each path segment to the tree if the segment
71
- # doesn't already exist. The segments can be either a folder or a file.
72
- paths = [path for path in file .filename .split ('/' ) if path ]
73
- for path in paths :
74
-
75
- # Add a child to the node
76
- if not len (node_path ['children' ]) or node_path ['children' ][- 1 ]['text' ] != path :
77
-
78
- new_node = {'text' : path , 'children' : []}
79
-
80
- date = '%d-%02d-%02d %02d:%02d:%02d' % file .date_time [:6 ]
81
- size = sizeof_fmt (int (file .file_size )) if file .file_size else ''
82
- new_node ['data' ] = {'date' : date , 'size' : size }
51
+ }
52
+
53
+ for obj in obj_list :
54
+
55
+ # For each object, always start from the root of the tree
56
+ parent = tree_root
57
+ path_from_root = obj .filename
58
+ is_folder = path_from_root [- 1 ] == '/'
59
+ path_segments = [segment for segment in path_from_root .split ('/' ) if segment ]
60
+ last_index = len (path_segments ) - 1
61
+
62
+ # Iterate through the path segments list. Add the segment to tree if not already there
63
+ # and update the details with the current object if it is the last one along the path.
64
+ for index , segment in enumerate (path_segments ):
65
+
66
+ # Check if the segment has already been added
67
+ siblings = parent .get ('children' , [])
68
+ current_node = self .find_node_among_siblings (segment , siblings )
69
+
70
+ # Found
71
+ if current_node :
72
+ if index == last_index :
73
+ # If it is the last segment, this node must be a folder and represents the
74
+ # current object. Update it with the objects' info and break.
75
+ assert is_folder
76
+ self .update_node_with_attributes (current_node , obj , is_folder = is_folder )
77
+ break
78
+ # Otherwise, jump to the next segment with the current node as the new parent
79
+ parent = current_node
80
+ continue
81
+
82
+ # Not found
83
+ new_node = {
84
+ 'text' : segment ,
85
+ 'children' : [],
86
+ }
87
+ if index == last_index :
88
+ # If it is the last segment, the node represents the current object. Update the
89
+ # it with the objects' info, add it to the siblings and break.
90
+ self .update_node_with_attributes (new_node , obj , is_folder = is_folder )
91
+ siblings .append (new_node )
92
+ break
93
+
94
+ # Otherwise, append the new node to tree, jump to the next segment with the current
95
+ # node as the new parent
96
+ siblings .append (new_node )
97
+ parent = new_node
98
+ continue
83
99
84
- if file .filename [- 1 ] == '/' :
85
- new_node ['icon' ] = icons_url + '/folder.png'
86
- else :
87
- ext = os .path .splitext (file .filename )[1 ].lstrip ('.' )
88
- if ext :
89
- ext = ext .lower ()
90
- if self .icon_exists_for_type (ext ):
91
- new_node ['icon' ] = '{}/file-ext-{}.png' .format (icons_url , ext )
92
- else :
93
- new_node ['icon' ] = '{}/file-ext-generic.png' .format (icons_url )
100
+ return [tree_root , ]
94
101
95
- node_path ['children' ].append (new_node )
102
+ def update_node_with_attributes (self , node : dict , obj : ZipInfo , is_folder : bool ) -> None :
103
+ """Update details (date, size, icon, etc.) of the node with the given object.
96
104
97
- node_path = new_node
98
- # Go one level deeper
99
- else :
100
- node_path = node_path [ 'children' ][ - 1 ]
105
+ :param node: the node to update
106
+ :param obj: the object that the node represents
107
+ :param is_folder: the folder flag
108
+ """
101
109
102
- return tree_root
110
+ date = '%d-%02d-%02d %02d:%02d:%02d' % obj .date_time [:6 ]
111
+ size = sizeof_fmt (int (obj .file_size )) if obj .file_size else ''
112
+
113
+ if is_folder :
114
+ icon_path = self .assets_url + '/img/folder.png'
115
+ else :
116
+ ext = (os .path .splitext (obj .filename )[1 ].lstrip ('.' )).lower ()
117
+ if self .icon_exists (ext ):
118
+ icon_path = '{}/img/file-ext-{}.png' .format (self .assets_url , ext )
119
+ else :
120
+ icon_path = '{}/img/file-ext-generic.png' .format (self .assets_url )
121
+
122
+ node .update ({
123
+ 'icon' : icon_path ,
124
+ 'data' : {
125
+ 'date' : date ,
126
+ 'size' : size ,
127
+ },
128
+ })
103
129
104
130
@staticmethod
105
- def icon_exists_for_type (ext : str ) -> bool :
131
+ def icon_exists (ext : str ) -> bool :
106
132
"""Check if an icon exists for the given file type. The extension string is converted to
107
133
lower case.
108
134
@@ -119,28 +145,44 @@ def icon_exists_for_type(ext: str) -> bool:
119
145
))
120
146
121
147
@staticmethod
122
- def sanitize_file_list ( file_list : list ) -> list :
148
+ def sanitize_obj_list ( obj_list : list ) -> list :
123
149
"""Remove macOS system and temporary files. Current implementation only removes '__MACOSX/'
124
150
and '.DS_Store'. If necessary, extend the sanitizer to exclude more file types.
125
151
126
- :param file_list: the list of the path for each file and folder in the zip
152
+ :param obj_list: a list of full paths for each file and folder in the zip
127
153
:rtype: ``list``
128
154
:return: a sanitized list
129
155
"""
130
156
131
- sanitized_file_list = []
157
+ sanitized_obj_list = []
132
158
133
- for file in file_list :
159
+ for obj in obj_list :
134
160
135
- file_path = file .filename
161
+ obj_path = obj .filename
136
162
# Ignore macOS '__MACOSX' folder for zip file
137
- if file_path .startswith ('__MACOSX/' ):
163
+ if obj_path .startswith ('__MACOSX/' ):
138
164
continue
139
-
140
165
# Ignore macOS '.DS_STORE' file
141
- if file_path == '.DS_Store' or file_path .endswith ('/.DS_Store' ):
166
+ if obj_path == '.DS_Store' or obj_path .endswith ('/.DS_Store' ):
142
167
continue
143
168
144
- sanitized_file_list .append (file )
169
+ sanitized_obj_list .append (obj )
170
+
171
+ return sanitized_obj_list
172
+
173
+ @staticmethod
174
+ 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.
176
+
177
+ :param segment: the path segment
178
+ :param siblings: the list containing all added sibling nodes
179
+ :rtype: ``Union[dict, None]``
180
+ :return: the node if found or ``None`` otherwise
181
+ """
182
+
183
+ for sibling in siblings :
184
+
185
+ if sibling .get ('text' , '' ) == segment :
186
+ return sibling
145
187
146
- return sanitized_file_list
188
+ return None
0 commit comments