Skip to content

Commit e94b6fb

Browse files
authored
Merge pull request #23 from geekdinazor/main
feat: buckets & objects added
2 parents 6c403a6 + fd6f58b commit e94b6fb

File tree

7 files changed

+186
-6
lines changed

7 files changed

+186
-6
lines changed

finch/__main__.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from finch.error import show_error_dialog
2222
from finch.filelist import S3FileListFetchThread
2323
from finch.upload import UploadDialog
24+
from finch.widgets.search import SearchWidget
2425

2526

2627
class MainWindow(QMainWindow):
@@ -72,8 +73,11 @@ def __init__(self):
7273
self.layout = QVBoxLayout()
7374
self.layout.setAlignment(Qt.AlignTop)
7475
self.widget.setLayout(self.layout)
75-
76+
self.tree_widget_wrapper = QWidget()
77+
self.tree_widget_wrapper_lay = QVBoxLayout()
78+
self.tree_widget_wrapper.setLayout(self.tree_widget_wrapper_lay)
7679
self.fill_credentials()
80+
self.layout.addWidget(self.tree_widget_wrapper)
7781
self.setCentralWidget(self.widget)
7882

7983
center_window(self)
@@ -132,7 +136,7 @@ def show_s3_files(self, cred_index):
132136
self.removeToolBar(self.about_toolbar)
133137
self.removeToolBar(self.file_toolbar)
134138
self.file_toolbar = self.addToolBar("File")
135-
self.layout.removeWidget(self.tree_widget)
139+
self.tree_widget_wrapper_lay.removeWidget(self.tree_widget)
136140
upload_file_action = QAction(self)
137141
upload_file_action.setText("&Upload")
138142
upload_file_action.setIcon(QIcon(resource_path('img/upload.svg')))
@@ -161,12 +165,18 @@ def show_s3_files(self, cred_index):
161165
refresh_action.setIcon(QIcon(resource_path('img/refresh.svg')))
162166
refresh_action.triggered.connect(self.refresh_ui)
163167

168+
search_action = QAction(self)
169+
search_action.setText("&Search")
170+
search_action.setIcon(QIcon(resource_path('img/search.svg')))
171+
search_action.triggered.connect(self.search)
172+
164173
self.file_toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
165174
self.file_toolbar.addAction(upload_file_action)
166175
self.file_toolbar.addAction(create_action)
167176
self.file_toolbar.addAction(delete_action)
168177
self.file_toolbar.addAction(download_action)
169178
self.file_toolbar.addAction(refresh_action)
179+
self.file_toolbar.addAction(search_action)
170180

171181
self.about_toolbar = self.addToolBar("About")
172182
self.about_toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
@@ -193,7 +203,7 @@ def show_s3_files(self, cred_index):
193203
self.tree_widget.itemExpanded.connect(self.add_files_to_tree)
194204
self.tree_widget.selectionModel().selectionChanged.connect(self.handle_selection)
195205

196-
self.layout.addWidget(self.tree_widget)
206+
self.tree_widget_wrapper_lay.addWidget(self.tree_widget)
197207

198208
self.add_buckets_to_tree()
199209

@@ -468,6 +478,19 @@ def refresh_ui(self) -> None:
468478
""" Refreshes the file treeview """
469479
self.removeToolBar(self.file_toolbar)
470480
self.show_s3_files(self.credential_selector.currentIndex())
481+
self.search_widget = SearchWidget(main_widget=self)
482+
if self.layout.itemAt(2):
483+
if isinstance(self.layout.itemAt(2).widget(), SearchWidget):
484+
for idx, action in enumerate(self.file_toolbar.actions()):
485+
if idx in [5]:
486+
action.setDisabled(True)
487+
488+
def search(self):
489+
self.search_widget = SearchWidget(main_widget=self)
490+
for idx, action in enumerate(self.file_toolbar.actions()):
491+
if idx in [5]:
492+
action.setDisabled(True)
493+
self.layout.addWidget(self.search_widget)
471494

472495
def open_about_window(self) -> None:
473496
""" Open about window """

finch/common.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ def format_object_name(filename: str) -> str:
7474

7575
def format_datetime(dt: datetime) -> str:
7676
""" Function for format dates """
77-
return dt.strftime(DATETIME_FORMAT)
77+
if dt:
78+
return dt.strftime(DATETIME_FORMAT)
79+
else:
80+
return ''
7881

7982
def remove_trailing_zeros(x: str) -> str:
8083
""" Function for removing trailing zeros from floats """

finch/filelist.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import json
22

3-
from PyQt5.QtCore import QThread, pyqtSignal, Qt
3+
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QEventLoop
44
from PyQt5.QtWidgets import QTreeWidgetItem
55

6-
from finch.common import s3_session, ObjectType
76
from finch.common import StringUtils
7+
from finch.common import s3_session, ObjectType
88

99

1010
class S3FileListFetchThread(QThread):
@@ -50,3 +50,4 @@ def run(self):
5050
self.file_list_fetched.emit(json.dumps(_obj), self.item)
5151
else:
5252
self.file_list_fetched.emit(json.dumps(_obj), self.item)
53+

finch/img/close.svg

+4
Loading

finch/img/search.svg

+4
Loading

finch/widgets/__init__.py

Whitespace-only changes.

finch/widgets/search.py

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import json
2+
3+
from PyQt5.QtGui import QIcon
4+
from PyQt5.QtWidgets import (
5+
QWidget, QHBoxLayout, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem, QStyle
6+
)
7+
8+
from finch.common import s3_session, ObjectType, StringUtils, resource_path
9+
10+
11+
class SearchWidget(QWidget):
12+
def __init__(self, main_widget: QWidget):
13+
super().__init__()
14+
self.main_widget = main_widget
15+
self.icon_type = self._initialize_icons()
16+
self._init_ui()
17+
18+
def showEvent(self, event):
19+
"""Ensure search input gets focus when the widget is shown."""
20+
super().showEvent(event)
21+
self.search_input.setFocus()
22+
23+
def close(self):
24+
super().close()
25+
for idx, action in enumerate(self.main_widget.file_toolbar.actions()):
26+
if idx in [5]:
27+
action.setDisabled(False)
28+
self.main_widget.layout.removeWidget(self)
29+
30+
31+
def _initialize_icons(self):
32+
"""Initialize icon mapping for different object types."""
33+
style = self.style()
34+
return {
35+
ObjectType.FILE: style.standardIcon(QStyle.SP_FileIcon),
36+
ObjectType.FOLDER: style.standardIcon(QStyle.SP_DirIcon),
37+
ObjectType.BUCKET: style.standardIcon(QStyle.SP_DirIcon),
38+
}
39+
40+
def _init_ui(self):
41+
"""Initialize UI components."""
42+
layout = QHBoxLayout()
43+
self.search_input = QLineEdit(placeholderText="Search")
44+
self.search_input.returnPressed.connect(self._on_search)
45+
self.search_button = QPushButton("Search")
46+
self.search_button.clicked.connect(self._on_search)
47+
self.close_button = QPushButton("")
48+
self.close_button.setIcon(QIcon(resource_path('img/close.svg')))
49+
self.close_button.setFlat(True)
50+
self.close_button.setStyleSheet("QPushButton { background-color: transparent }")
51+
self.close_button.clicked.connect(self.close)
52+
53+
layout.addWidget(self.search_input)
54+
layout.addWidget(self.search_button)
55+
layout.addWidget(self.close_button)
56+
self.setLayout(layout)
57+
58+
def _on_search(self):
59+
"""Handle search button click."""
60+
search_term = self.search_input.text()
61+
self.main_widget.tree_widget.clear()
62+
self._search_and_populate(search_term)
63+
64+
for i in range(self.main_widget.tree_widget.topLevelItemCount()):
65+
self._expand_and_select(self.main_widget.tree_widget.topLevelItem(i), search_term)
66+
67+
def _search_and_populate(self, search_term):
68+
"""Search S3 and populate the tree widget."""
69+
buckets = self._get_s3_buckets()
70+
items = self._search_s3_objects(buckets, search_term)
71+
72+
for bucket in buckets:
73+
bucket_item = self._create_tree_item(
74+
name=bucket['Name'], object_type=ObjectType.BUCKET, date=bucket['CreationDate']
75+
)
76+
77+
self.main_widget.tree_widget.addTopLevelItem(bucket_item)
78+
79+
bucket_objects = [
80+
(item['Key'], item['Size'], item['LastModified'])
81+
for name, item in items if name == bucket['Name']
82+
]
83+
tree_structure = self._build_tree_structure(bucket_objects)
84+
self._add_items_to_tree(bucket_item, tree_structure)
85+
86+
def _get_s3_buckets(self):
87+
"""Retrieve list of S3 buckets."""
88+
return s3_session.resource.meta.client.list_buckets()['Buckets']
89+
90+
def _search_s3_objects(self, buckets, search_term):
91+
"""Search for objects in S3 matching the search term."""
92+
items = []
93+
for bucket in buckets:
94+
paginator = s3_session.resource.meta.client.get_paginator('list_objects_v2')
95+
for obj in paginator.paginate(Bucket=bucket['Name']).search(
96+
f"Contents[?contains(Key, `{json.dumps(search_term)}`)][]"
97+
):
98+
if obj:
99+
items.append((bucket['Name'], obj))
100+
return items
101+
102+
def _build_tree_structure(self, objects):
103+
"""Build a nested dictionary representing the folder structure."""
104+
tree = {}
105+
for path, size, date in objects:
106+
current = tree
107+
*folders, filename = path.split('/')
108+
for folder in folders:
109+
current = current.setdefault(folder, {})
110+
current[filename] = {"_info": (size, date)}
111+
return tree
112+
113+
def _add_items_to_tree(self, parent_item, tree_dict):
114+
"""Recursively add items to the tree widget."""
115+
for key, value in tree_dict.items():
116+
if key == "_info":
117+
continue
118+
119+
item = self._create_tree_item(name=key, object_type=ObjectType.FOLDER)
120+
parent_item.addChild(item)
121+
122+
if "_info" in value:
123+
size, date = value["_info"]
124+
item = self._create_tree_item(name=key, object_type=ObjectType.FILE, size=size, date=date)
125+
126+
self._add_items_to_tree(item, value)
127+
128+
def _expand_and_select(self, item, search_term):
129+
"""Recursively expand and select matching items."""
130+
if item.childCount():
131+
item.setExpanded(True)
132+
if search_term in item.text(0):
133+
item.setSelected(True)
134+
for i in range(item.childCount()):
135+
self._expand_and_select(item.child(i), search_term)
136+
137+
def _create_tree_item(self, name, object_type, size=0, date=None):
138+
"""Create a QTreeWidgetItem with the given texts, type, icon, size, and date."""
139+
item = QTreeWidgetItem()
140+
item.setText(0, name)
141+
item.setIcon(0, self.icon_type[object_type])
142+
item.setText(1, object_type)
143+
item.setText(2, StringUtils.format_size(size))
144+
item.setText(3, StringUtils.format_datetime(date))
145+
return item

0 commit comments

Comments
 (0)