diff --git a/retoors/services/file_service.py b/retoors/services/file_service.py index f820533..cbb2158 100644 --- a/retoors/services/file_service.py +++ b/retoors/services/file_service.py @@ -63,6 +63,12 @@ class FileService: # Normalize path if path and not path.endswith('/'): path += '/' + # Update last_accessed for the folder if path is not root + if path: + folder_path = path.rstrip('/') + if folder_path in metadata and metadata[folder_path].get("type") == "dir": + metadata[folder_path]["last_accessed"] = datetime.datetime.now().isoformat() + await self._save_metadata(user_email, metadata) items = [] seen = set() for item_path, item_meta in metadata.items(): @@ -91,6 +97,7 @@ class FileService: "type": "dir", "created_at": datetime.datetime.now().isoformat(), "modified_at": datetime.datetime.now().isoformat(), + "last_accessed": datetime.datetime.now().isoformat(), } await self._save_metadata(user_email, metadata) logger.info(f"create_folder: Folder created: {folder_path}") @@ -115,6 +122,7 @@ class FileService: "blob_location": {"drive": drive, "path": f"{dir1}/{dir2}/{dir3}/{hash}"}, "created_at": datetime.datetime.now().isoformat(), "modified_at": datetime.datetime.now().isoformat(), + "last_accessed": datetime.datetime.now().isoformat(), } await self._save_metadata(user_email, metadata) logger.info(f"upload_file: File uploaded to drive {drive}: {file_path}") @@ -127,6 +135,9 @@ class FileService: logger.warning(f"download_file: File not found in metadata: {file_path}") return None item_meta = metadata[file_path] + # Update last_accessed + item_meta["last_accessed"] = datetime.datetime.now().isoformat() + await self._save_metadata(user_email, metadata) blob_loc = item_meta["blob_location"] blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"] if not blob_path.exists(): @@ -144,6 +155,9 @@ class FileService: logger.warning(f"read_file_content: File not found in metadata: {file_path}") return None item_meta = metadata[file_path] + # Update last_accessed + item_meta["last_accessed"] = datetime.datetime.now().isoformat() + await self._save_metadata(user_email, metadata) blob_loc = item_meta["blob_location"] blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"] if not blob_path.exists(): @@ -168,6 +182,9 @@ class FileService: logger.warning(f"read_file_content: File not found in metadata: {file_path}") return None item_meta = metadata[file_path] + # Update last_accessed + item_meta["last_accessed"] = datetime.datetime.now().isoformat() + await self._save_metadata(user_email, metadata) blob_loc = item_meta["blob_location"] blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"] if not blob_path.exists(): @@ -303,6 +320,7 @@ class FileService: "type": "dir", "created_at": datetime.datetime.now().isoformat(), "modified_at": datetime.datetime.now().isoformat(), + "last_accessed": datetime.datetime.now().isoformat(), } migrated_count += 1 for file_name in files: @@ -330,9 +348,28 @@ class FileService: "blob_location": {"drive": drive, "path": f"{dir1}/{dir2}/{dir3}/{hash}"}, "created_at": datetime.datetime.fromtimestamp(full_file_path.stat().st_ctime).isoformat(), "modified_at": datetime.datetime.fromtimestamp(full_file_path.stat().st_mtime).isoformat(), + "last_accessed": datetime.datetime.fromtimestamp(full_file_path.stat().st_atime).isoformat(), } migrated_count += 1 await self._save_metadata(user_email, metadata) logger.info(f"Migrated {migrated_count} items for {user_email}") # Optionally remove old dir # shutil.rmtree(old_user_dir) + + async def get_recent_files(self, user_email: str, limit: int = 50) -> list: + """Gets the most recently accessed files and folders for the user.""" + metadata = await self._load_metadata(user_email) + items = [] + for path, meta in metadata.items(): + if meta.get("type") in ("file", "dir"): + last_accessed = meta.get("last_accessed", meta.get("modified_at", "")) + items.append({ + "path": path, + "name": Path(path).name, + "is_dir": meta["type"] == "dir", + "size": meta.get("size", 0) if meta["type"] == "file" else 0, + "last_accessed": last_accessed, + }) + # Sort by last_accessed descending + items.sort(key=lambda x: x["last_accessed"], reverse=True) + return items[:limit] diff --git a/retoors/services/user_service.py b/retoors/services/user_service.py index 3bc99ce..fab3bf5 100644 --- a/retoors/services/user_service.py +++ b/retoors/services/user_service.py @@ -211,3 +211,48 @@ class UserService: await self._storage_manager.save_user(email, user) else: await self._save_users() + + async def add_favorite(self, email: str, file_path: str) -> bool: + user = await self.get_user_by_email(email) + if not user: + return False + + if "favorites" not in user: + user["favorites"] = [] + + if file_path not in user["favorites"]: + user["favorites"].append(file_path) + + if self.use_isolated_storage: + await self._storage_manager.save_user(email, user) + else: + await self._save_users() + + return True + + async def remove_favorite(self, email: str, file_path: str) -> bool: + user = await self.get_user_by_email(email) + if not user: + return False + + if "favorites" in user and file_path in user["favorites"]: + user["favorites"].remove(file_path) + + if self.use_isolated_storage: + await self._storage_manager.save_user(email, user) + else: + await self._save_users() + + return True + + async def get_favorites(self, email: str) -> List[str]: + user = await self.get_user_by_email(email) + if not user: + return [] + return user.get("favorites", []) + + async def is_favorite(self, email: str, file_path: str) -> bool: + user = await self.get_user_by_email(email) + if not user: + return False + return file_path in user.get("favorites", []) diff --git a/retoors/templates/pages/index.html b/retoors/templates/pages/index.html index 137806a..eaa2d9e 100644 --- a/retoors/templates/pages/index.html +++ b/retoors/templates/pages/index.html @@ -8,6 +8,7 @@ {% block content %}
+ {#

Your files, safe and accessible everywhere

@@ -89,9 +90,9 @@
- + #}
-

Perfect for every need

+

Storage for every need

Families Icon diff --git a/retoors/templates/pages/recent.html b/retoors/templates/pages/recent.html index 1ee754c..c74e1ec 100644 --- a/retoors/templates/pages/recent.html +++ b/retoors/templates/pages/recent.html @@ -52,8 +52,14 @@ Folder Icon {{ item.name }} {% else %} - File Icon - {{ item.name }} + File Icon + {% if item.is_editable %} + {{ item.name }} + {% elif item.is_viewable %} + {{ item.name }} + {% else %} + {{ item.name }} + {% endif %} {% endif %} {{ user.email }} @@ -119,4 +125,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/retoors/views/site.py b/retoors/views/site.py index a9701ff..082ab6d 100644 --- a/retoors/views/site.py +++ b/retoors/views/site.py @@ -142,8 +142,22 @@ class SiteView(web.View): @login_required async def recent(self): + user_email = self.request["user"]["email"] + file_service = self.request.app["file_service"] + recent_files = await file_service.get_recent_files(user_email) + + # Determine editable and viewable files based on extension + editable_extensions = {'.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.log', '.sh', '.bat', '.ps1', '.php', '.rb', '.java', '.c', '.cpp', '.h', '.hpp'} + viewable_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a'} + for item in recent_files: + if not item['is_dir']: + from pathlib import Path + ext = Path(item['name']).suffix.lower() + item['is_editable'] = ext in editable_extensions + item['is_viewable'] = ext in viewable_extensions + return aiohttp_jinja2.render_template( - "pages/recent.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "recent"} + "pages/recent.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "recent", "recent_files": recent_files} ) @login_required @@ -186,11 +200,13 @@ class FileBrowserView(web.View): # Determine editable and viewable files based on extension editable_extensions = {'.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.log', '.sh', '.bat', '.ps1', '.php', '.rb', '.java', '.c', '.cpp', '.h', '.hpp'} viewable_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a'} + user_service = self.request.app["user_service"] for item in files: if not item['is_dir']: ext = Path(item['name']).suffix.lower() item['is_editable'] = ext in editable_extensions item['is_viewable'] = ext in viewable_extensions + item['is_favorite'] = await user_service.is_favorite(user_email, item['path']) success_message = self.request.query.get("success") error_message = self.request.query.get("error") @@ -348,6 +364,19 @@ class FileBrowserView(web.View): logger.error(f"FileBrowserView: Failed to generate any share links for user {user_email}") return json_response({"error": "Failed to generate share links for any selected items"}, status=500) + elif route_name == "toggle_favorite": + data = await self.request.json() + file_path = data.get("file_path") + if not file_path: + return json_response({"error": "File path is required"}, status=400) + user_service = self.request.app["user_service"] + is_fav = await user_service.is_favorite(user_email, file_path) + if is_fav: + await user_service.remove_favorite(user_email, file_path) + else: + await user_service.add_favorite(user_email, file_path) + return json_response({"is_favorite": not is_fav}) + logger.warning(f"FileBrowserView: Unknown file action for POST request: {route_name}") raise web.HTTPBadRequest(text="Unknown file action")