|
1 | 1 | import os
|
2 |
| -import zipfile |
| 2 | +from typing import List |
| 3 | +from zipfile import ZipFile |
3 | 4 |
|
4 | 5 | from mako.lookup import TemplateLookup
|
5 | 6 |
|
6 |
| -from mfr.core import extension |
7 | 7 | from mfr.core.utils import sizeof_fmt
|
| 8 | +from mfr.core.extension import BaseRenderer |
8 | 9 |
|
9 | 10 |
|
10 |
| -class ZipRenderer(extension.BaseRenderer): |
| 11 | +class ZipRenderer(BaseRenderer): |
11 | 12 |
|
12 | 13 | TEMPLATE = TemplateLookup(
|
13 | 14 | directories=[
|
14 | 15 | os.path.join(os.path.dirname(__file__), 'templates')
|
15 | 16 | ]).get_template('viewer.mako')
|
16 | 17 |
|
| 18 | + @property |
| 19 | + def file_required(self): |
| 20 | + return True |
| 21 | + |
| 22 | + @property |
| 23 | + def cache_result(self): |
| 24 | + return False |
| 25 | + |
17 | 26 | 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')] |
20 | 27 |
|
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) |
22 | 34 |
|
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". |
24 | 37 |
|
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. |
26 | 41 |
|
27 |
| - self.icons_url = self.assets_url + '/img' |
| 42 | + A.zip |
| 43 | + --- A/ |
| 44 | + --- A/aa.png |
| 45 | + --- B/ab.png |
28 | 46 |
|
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 = [{ |
30 | 60 | 'text': self.metadata.name + self.metadata.ext,
|
31 |
| - 'icon': self.icons_url + '/file_extension_zip.png', |
| 61 | + 'icon': icons_url + '/file-ext-zip.png', |
32 | 62 | 'children': []
|
33 | 63 | }]
|
34 | 64 |
|
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. |
37 | 72 | paths = [path for path in file.filename.split('/') if path]
|
38 | 73 | 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': []} |
42 | 74 |
|
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: |
46 | 77 |
|
47 |
| - # create new node |
48 |
| - new_node['data'] = {'date': date, 'size': size} |
| 78 | + new_node = {'text': path, 'children': []} |
49 | 79 |
|
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) |
52 | 92 | 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) |
59 | 94 |
|
60 |
| - node_path['children'].append(new_node) |
| 95 | + node_path['children'].append(new_node) |
61 | 96 |
|
62 | 97 | node_path = new_node
|
| 98 | + # Go one level deeper |
63 | 99 | else:
|
64 |
| - # "go deeper" to get children of children. |
65 | 100 | node_path = node_path['children'][-1]
|
66 | 101 |
|
67 |
| - return tree_data |
| 102 | + return tree_root |
68 | 103 |
|
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. |
72 | 108 |
|
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) |
76 | 145 |
|
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 |
0 commit comments