Skip to content

Commit c4c3ff3

Browse files
committed
Rewrite the render.py for zip for better clarity
- Update code for previous assets refactor - Improve import styles and naming for vars/funcs - Add a sanitizer to remove system files/folders - Update docstr, comments and TODO - Discover a bug that the tree building algorithm only works when files in the zip file are sorted in increasing alphabetic order
1 parent abadcce commit c4c3ff3

File tree

2 files changed

+126
-51
lines changed

2 files changed

+126
-51
lines changed

mfr/extensions/zip/render.py

Lines changed: 108 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,146 @@
11
import os
2-
import zipfile
2+
from typing import List
3+
from zipfile import ZipFile
34

45
from mako.lookup import TemplateLookup
56

6-
from mfr.core import extension
77
from mfr.core.utils import sizeof_fmt
8+
from mfr.core.extension import BaseRenderer
89

910

10-
class ZipRenderer(extension.BaseRenderer):
11+
class ZipRenderer(BaseRenderer):
1112

1213
TEMPLATE = TemplateLookup(
1314
directories=[
1415
os.path.join(os.path.dirname(__file__), 'templates')
1516
]).get_template('viewer.mako')
1617

18+
@property
19+
def file_required(self):
20+
return True
21+
22+
@property
23+
def cache_result(self):
24+
return False
25+
1726
def render(self):
18-
zip_file = zipfile.ZipFile(self.file_path, 'r')
19-
files = [file for file in zip_file.filelist if not file.filename.startswith('__MACOSX')]
2027

21-
data = self.filelist_to_tree(files)
28+
zip_file = ZipFile(self.file_path, 'r')
29+
30+
file_list = self.sanitize_file_list(zip_file.filelist)
31+
file_tree = self.file_list_to_tree(file_list)
32+
33+
return self.TEMPLATE.render(data=file_tree, base=self.assets_url)
2234

23-
return self.TEMPLATE.render(data=data, base=self.assets_url)
35+
def file_list_to_tree(self, file_list: list) -> List[dict]:
36+
"""Build the file tree and return a "tree".
2437
25-
def filelist_to_tree(self, files):
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.
2641
27-
self.icons_url = self.assets_url + '/img'
42+
A.zip
43+
--- A/
44+
--- A/aa.png
45+
--- B/ab.png
2846
29-
tree_data = [{
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
52+
:rtype: ``List[dict]``
53+
:return: a "tree" in form of a list which contains one dictionary as the root node
54+
"""
55+
56+
icons_url = self.assets_url + '/img'
57+
58+
# Build the root of the file tree
59+
tree_root = [{
3060
'text': self.metadata.name + self.metadata.ext,
31-
'icon': self.icons_url + '/file_extension_zip.png',
61+
'icon': icons_url + '/file-ext-zip.png',
3262
'children': []
3363
}]
3464

35-
for file in files:
36-
node_path = tree_data[0]
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.
3772
paths = [path for path in file.filename.split('/') if path]
3873
for path in paths:
39-
if not len(node_path['children']) or node_path['children'][-1]['text'] != path:
40-
# Add a child
41-
new_node = {'text': path, 'children': []}
4274

43-
if new_node['text']: # If not a placeholder/"root" directory.
44-
date = '%d-%02d-%02d %02d:%02d:%02d' % file.date_time[:6]
45-
size = sizeof_fmt(int(file.file_size)) if file.file_size else ''
75+
# Add a child to the node
76+
if not len(node_path['children']) or node_path['children'][-1]['text'] != path:
4677

47-
# create new node
48-
new_node['data'] = {'date': date, 'size': size}
78+
new_node = {'text': path, 'children': []}
4979

50-
if file.filename[-1] == '/':
51-
new_node['icon'] = self.icons_url + '/folder.png'
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}
83+
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)
5292
else:
53-
ext = os.path.splitext(file.filename)[1].lstrip('.')
54-
if check_icon_ext(ext):
55-
new_node['icon'] = \
56-
self.icons_url + '/file_extension_{}.png'.format(ext)
57-
else:
58-
new_node['icon'] = self.icons_url + '/generic-file.png'
93+
new_node['icon'] = '{}/file-ext-generic.png'.format(icons_url)
5994

60-
node_path['children'].append(new_node)
95+
node_path['children'].append(new_node)
6196

6297
node_path = new_node
98+
# Go one level deeper
6399
else:
64-
# "go deeper" to get children of children.
65100
node_path = node_path['children'][-1]
66101

67-
return tree_data
102+
return tree_root
68103

69-
@property
70-
def file_required(self):
71-
return True
104+
@staticmethod
105+
def icon_exists_for_type(ext: str) -> bool:
106+
"""Check if an icon exists for the given file type. The extension string is converted to
107+
lower case.
72108
73-
@property
74-
def cache_result(self):
75-
return True
109+
:param ext: the file extension str
110+
:rtype: ``bool``
111+
:return: ``True`` if found; ``False`` otherwise
112+
"""
113+
114+
return os.path.isfile(os.path.join(
115+
os.path.dirname(__file__),
116+
'static',
117+
'img',
118+
'file-ext-{}.png'.format(ext.lower())
119+
))
120+
121+
@staticmethod
122+
def sanitize_file_list(file_list: list) -> list:
123+
"""Remove macOS system and temporary files. Current implementation only removes '__MACOSX/'
124+
and '.DS_Store'. If necessary, extend the sanitizer to exclude more file types.
125+
126+
:param file_list: the list of the path for each file and folder in the zip
127+
:rtype: ``list``
128+
:return: a sanitized list
129+
"""
130+
131+
sanitized_file_list = []
132+
133+
for file in file_list:
134+
135+
file_path = file.filename
136+
# Ignore macOS '__MACOSX' folder for zip file
137+
if file_path.startswith('__MACOSX/'):
138+
continue
139+
140+
# Ignore macOS '.DS_STORE' file
141+
if file_path == '.DS_Store' or file_path.endswith('/.DS_Store'):
142+
continue
143+
144+
sanitized_file_list.append(file)
76145

77-
def check_icon_ext(ext):
78-
return os.path.isfile(os.path.join(os.path.dirname(__file__), 'static', 'img', 'icons',
79-
'file_extension_{}.png'.format(ext)))
146+
return sanitized_file_list

mfr/extensions/zip/templates/viewer.mako

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
</div>
1414

1515
<script>
16+
1617
$('#ziptree').jstree({
1718
"core" : {
1819
"data" : ${data},
@@ -22,21 +23,28 @@
2223
"search"
2324
],
2425
'table': {
25-
'columns': [
26-
{'header': "Name", 'width': '100%'}, // these widths are strange but work.
27-
{'header': "Size", 'width': 80, 'value': "size"},
28-
{'header': "Date", 'width': 150, 'value': "date"}
29-
],
26+
'columns': [
27+
{'header': "Name", 'width': '100%'},
28+
{'header': "Size", 'width': 100, 'value': "size"},
29+
{'header': "Date", 'width': 250, 'value': "date"}
30+
]
3031
}
3132
});
33+
3234
var to = false;
3335
$('#ziptree_search').keyup(function () {
34-
if(to) { clearTimeout(to); }
35-
to = setTimeout(function () {
36-
var v = $('#ziptree_search').val();
37-
$('#ziptree').jstree(true).search(v);
38-
}, 250);
36+
if (to) {
37+
clearTimeout(to);
38+
}
39+
to = setTimeout(
40+
function () {
41+
var v = $('#ziptree_search').val();
42+
$('#ziptree').jstree(true).search(v);
43+
},
44+
250
45+
);
3946
});
47+
4048
</script>
4149

4250
<script src="/static/js/mfr.js"></script>

0 commit comments

Comments
 (0)