Skip to content

Commit

Permalink
Merge pull request #23 from geekdinazor/main
Browse files Browse the repository at this point in the history
feat: buckets & objects added
  • Loading branch information
geekdinazor authored Oct 19, 2024
2 parents 6c403a6 + fd6f58b commit e94b6fb
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 6 deletions.
29 changes: 26 additions & 3 deletions finch/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from finch.error import show_error_dialog
from finch.filelist import S3FileListFetchThread
from finch.upload import UploadDialog
from finch.widgets.search import SearchWidget


class MainWindow(QMainWindow):
Expand Down Expand Up @@ -72,8 +73,11 @@ def __init__(self):
self.layout = QVBoxLayout()
self.layout.setAlignment(Qt.AlignTop)
self.widget.setLayout(self.layout)

self.tree_widget_wrapper = QWidget()
self.tree_widget_wrapper_lay = QVBoxLayout()
self.tree_widget_wrapper.setLayout(self.tree_widget_wrapper_lay)
self.fill_credentials()
self.layout.addWidget(self.tree_widget_wrapper)
self.setCentralWidget(self.widget)

center_window(self)
Expand Down Expand Up @@ -132,7 +136,7 @@ def show_s3_files(self, cred_index):
self.removeToolBar(self.about_toolbar)
self.removeToolBar(self.file_toolbar)
self.file_toolbar = self.addToolBar("File")
self.layout.removeWidget(self.tree_widget)
self.tree_widget_wrapper_lay.removeWidget(self.tree_widget)
upload_file_action = QAction(self)
upload_file_action.setText("&Upload")
upload_file_action.setIcon(QIcon(resource_path('img/upload.svg')))
Expand Down Expand Up @@ -161,12 +165,18 @@ def show_s3_files(self, cred_index):
refresh_action.setIcon(QIcon(resource_path('img/refresh.svg')))
refresh_action.triggered.connect(self.refresh_ui)

search_action = QAction(self)
search_action.setText("&Search")
search_action.setIcon(QIcon(resource_path('img/search.svg')))
search_action.triggered.connect(self.search)

self.file_toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
self.file_toolbar.addAction(upload_file_action)
self.file_toolbar.addAction(create_action)
self.file_toolbar.addAction(delete_action)
self.file_toolbar.addAction(download_action)
self.file_toolbar.addAction(refresh_action)
self.file_toolbar.addAction(search_action)

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

self.layout.addWidget(self.tree_widget)
self.tree_widget_wrapper_lay.addWidget(self.tree_widget)

self.add_buckets_to_tree()

Expand Down Expand Up @@ -468,6 +478,19 @@ def refresh_ui(self) -> None:
""" Refreshes the file treeview """
self.removeToolBar(self.file_toolbar)
self.show_s3_files(self.credential_selector.currentIndex())
self.search_widget = SearchWidget(main_widget=self)
if self.layout.itemAt(2):
if isinstance(self.layout.itemAt(2).widget(), SearchWidget):
for idx, action in enumerate(self.file_toolbar.actions()):
if idx in [5]:
action.setDisabled(True)

def search(self):
self.search_widget = SearchWidget(main_widget=self)
for idx, action in enumerate(self.file_toolbar.actions()):
if idx in [5]:
action.setDisabled(True)
self.layout.addWidget(self.search_widget)

def open_about_window(self) -> None:
""" Open about window """
Expand Down
5 changes: 4 additions & 1 deletion finch/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ def format_object_name(filename: str) -> str:

def format_datetime(dt: datetime) -> str:
""" Function for format dates """
return dt.strftime(DATETIME_FORMAT)
if dt:
return dt.strftime(DATETIME_FORMAT)
else:
return ''

def remove_trailing_zeros(x: str) -> str:
""" Function for removing trailing zeros from floats """
Expand Down
5 changes: 3 additions & 2 deletions finch/filelist.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import json

from PyQt5.QtCore import QThread, pyqtSignal, Qt
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QEventLoop
from PyQt5.QtWidgets import QTreeWidgetItem

from finch.common import s3_session, ObjectType
from finch.common import StringUtils
from finch.common import s3_session, ObjectType


class S3FileListFetchThread(QThread):
Expand Down Expand Up @@ -50,3 +50,4 @@ def run(self):
self.file_list_fetched.emit(json.dumps(_obj), self.item)
else:
self.file_list_fetched.emit(json.dumps(_obj), self.item)

4 changes: 4 additions & 0 deletions finch/img/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions finch/img/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added finch/widgets/__init__.py
Empty file.
145 changes: 145 additions & 0 deletions finch/widgets/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import json

from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
QWidget, QHBoxLayout, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem, QStyle
)

from finch.common import s3_session, ObjectType, StringUtils, resource_path


class SearchWidget(QWidget):
def __init__(self, main_widget: QWidget):
super().__init__()
self.main_widget = main_widget
self.icon_type = self._initialize_icons()
self._init_ui()

def showEvent(self, event):
"""Ensure search input gets focus when the widget is shown."""
super().showEvent(event)
self.search_input.setFocus()

def close(self):
super().close()
for idx, action in enumerate(self.main_widget.file_toolbar.actions()):
if idx in [5]:
action.setDisabled(False)
self.main_widget.layout.removeWidget(self)


def _initialize_icons(self):
"""Initialize icon mapping for different object types."""
style = self.style()
return {
ObjectType.FILE: style.standardIcon(QStyle.SP_FileIcon),
ObjectType.FOLDER: style.standardIcon(QStyle.SP_DirIcon),
ObjectType.BUCKET: style.standardIcon(QStyle.SP_DirIcon),
}

def _init_ui(self):
"""Initialize UI components."""
layout = QHBoxLayout()
self.search_input = QLineEdit(placeholderText="Search")
self.search_input.returnPressed.connect(self._on_search)
self.search_button = QPushButton("Search")
self.search_button.clicked.connect(self._on_search)
self.close_button = QPushButton("")
self.close_button.setIcon(QIcon(resource_path('img/close.svg')))
self.close_button.setFlat(True)
self.close_button.setStyleSheet("QPushButton { background-color: transparent }")
self.close_button.clicked.connect(self.close)

layout.addWidget(self.search_input)
layout.addWidget(self.search_button)
layout.addWidget(self.close_button)
self.setLayout(layout)

def _on_search(self):
"""Handle search button click."""
search_term = self.search_input.text()
self.main_widget.tree_widget.clear()
self._search_and_populate(search_term)

for i in range(self.main_widget.tree_widget.topLevelItemCount()):
self._expand_and_select(self.main_widget.tree_widget.topLevelItem(i), search_term)

def _search_and_populate(self, search_term):
"""Search S3 and populate the tree widget."""
buckets = self._get_s3_buckets()
items = self._search_s3_objects(buckets, search_term)

for bucket in buckets:
bucket_item = self._create_tree_item(
name=bucket['Name'], object_type=ObjectType.BUCKET, date=bucket['CreationDate']
)

self.main_widget.tree_widget.addTopLevelItem(bucket_item)

bucket_objects = [
(item['Key'], item['Size'], item['LastModified'])
for name, item in items if name == bucket['Name']
]
tree_structure = self._build_tree_structure(bucket_objects)
self._add_items_to_tree(bucket_item, tree_structure)

def _get_s3_buckets(self):
"""Retrieve list of S3 buckets."""
return s3_session.resource.meta.client.list_buckets()['Buckets']

def _search_s3_objects(self, buckets, search_term):
"""Search for objects in S3 matching the search term."""
items = []
for bucket in buckets:
paginator = s3_session.resource.meta.client.get_paginator('list_objects_v2')
for obj in paginator.paginate(Bucket=bucket['Name']).search(
f"Contents[?contains(Key, `{json.dumps(search_term)}`)][]"
):
if obj:
items.append((bucket['Name'], obj))
return items

def _build_tree_structure(self, objects):
"""Build a nested dictionary representing the folder structure."""
tree = {}
for path, size, date in objects:
current = tree
*folders, filename = path.split('/')
for folder in folders:
current = current.setdefault(folder, {})
current[filename] = {"_info": (size, date)}
return tree

def _add_items_to_tree(self, parent_item, tree_dict):
"""Recursively add items to the tree widget."""
for key, value in tree_dict.items():
if key == "_info":
continue

item = self._create_tree_item(name=key, object_type=ObjectType.FOLDER)
parent_item.addChild(item)

if "_info" in value:
size, date = value["_info"]
item = self._create_tree_item(name=key, object_type=ObjectType.FILE, size=size, date=date)

self._add_items_to_tree(item, value)

def _expand_and_select(self, item, search_term):
"""Recursively expand and select matching items."""
if item.childCount():
item.setExpanded(True)
if search_term in item.text(0):
item.setSelected(True)
for i in range(item.childCount()):
self._expand_and_select(item.child(i), search_term)

def _create_tree_item(self, name, object_type, size=0, date=None):
"""Create a QTreeWidgetItem with the given texts, type, icon, size, and date."""
item = QTreeWidgetItem()
item.setText(0, name)
item.setIcon(0, self.icon_type[object_type])
item.setText(1, object_type)
item.setText(2, StringUtils.format_size(size))
item.setText(3, StringUtils.format_datetime(date))
return item

0 comments on commit e94b6fb

Please sign in to comment.