Edition de fichier distant en place, et journalisation de session
Edition SFTP facon WinSCP : clic droit sur un fichier distant > "Editer". Le fichier est telecharge dans un temporaire, ouvert avec l'application associee du systeme, et renvoye automatiquement a chaque enregistrement. Beaucoup d'editeurs enregistrent en remplacant le fichier plutot qu'en le reecrivant : le chemin surveille disparait alors du QFileSystemWatcher, il est donc reinscrit apres chaque renvoi pour que les enregistrements suivants fonctionnent aussi. Journalisation : un bouton dans la barre de l'onglet enregistre toute la session dans un fichier. Les sequences d'echappement ANSI sont retirees pour que le journal reste lisible dans un editeur de texte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1651
app/main_window.py
1651
app/main_window.py
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,13 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import tempfile
|
||||
import stat
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import paramiko
|
||||
from PySide6.QtCore import Qt, QThread, Signal, QMimeData, QPoint
|
||||
from PySide6.QtCore import Qt, QThread, Signal, QMimeData, QPoint, QFileSystemWatcher
|
||||
from PySide6.QtGui import QDrag, QKeySequence, QShortcut
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QToolButton, QTreeWidget,
|
||||
@@ -324,9 +325,11 @@ class FilePanel(QWidget):
|
||||
act_rename.setEnabled(has_selection)
|
||||
act_delete = menu.addAction("Supprimer (Suppr)")
|
||||
act_delete.setEnabled(has_selection)
|
||||
act_perms = None
|
||||
act_perms = act_edit = None
|
||||
if self.side == "remote":
|
||||
menu.addSeparator()
|
||||
act_edit = menu.addAction("Editer (renvoi automatique a l'enregistrement)")
|
||||
act_edit.setEnabled(has_selection)
|
||||
act_perms = menu.addAction("Permissions...")
|
||||
act_perms.setEnabled(has_selection)
|
||||
menu.addSeparator()
|
||||
@@ -340,6 +343,8 @@ class FilePanel(QWidget):
|
||||
self.rename_selected()
|
||||
elif action == act_delete:
|
||||
self.delete_selected()
|
||||
elif act_edit is not None and action == act_edit:
|
||||
self.edit_selected()
|
||||
elif act_perms is not None and action == act_perms:
|
||||
self.change_permissions()
|
||||
elif action == act_mkdir:
|
||||
@@ -361,6 +366,7 @@ class FilePanel(QWidget):
|
||||
def rename_selected(self) -> None: ...
|
||||
def delete_selected(self) -> None: ...
|
||||
def change_permissions(self) -> None: ...
|
||||
def edit_selected(self) -> None: ...
|
||||
|
||||
|
||||
class LocalFilePanel(FilePanel):
|
||||
@@ -453,6 +459,10 @@ class RemoteFilePanel(FilePanel):
|
||||
def __init__(self, sftp: paramiko.SFTPClient, parent=None):
|
||||
super().__init__("🌐 Distant", "remote", parent)
|
||||
self.sftp = sftp
|
||||
# fichiers ouverts pour edition : chemin local temporaire -> chemin distant
|
||||
self._edited: dict[str, str] = {}
|
||||
self._watcher = QFileSystemWatcher(self)
|
||||
self._watcher.fileChanged.connect(self._on_edited_file_changed)
|
||||
try:
|
||||
home = self.sftp.normalize(".")
|
||||
except Exception:
|
||||
@@ -546,6 +556,55 @@ class RemoteFilePanel(FilePanel):
|
||||
self.sftp.remove(sub)
|
||||
self.sftp.rmdir(path)
|
||||
|
||||
# ---------- edition en place ----------
|
||||
def edit_selected(self) -> None:
|
||||
"""Telecharge le fichier, l'ouvre dans l'editeur par defaut du systeme et le
|
||||
renvoie automatiquement a chaque enregistrement (facon WinSCP)."""
|
||||
entries = [(n, d) for n, d in self.selected_entries() if not d]
|
||||
if not entries:
|
||||
QMessageBox.information(self, "Edition", "Selectionnez un fichier (pas un dossier).")
|
||||
return
|
||||
name = entries[0][0]
|
||||
remote_path = self.join(self.current_path, name)
|
||||
|
||||
local_dir = Path(tempfile.gettempdir()) / "pyssh-edit"
|
||||
local_dir.mkdir(parents=True, exist_ok=True)
|
||||
local_path = local_dir / name
|
||||
try:
|
||||
self.sftp.get(remote_path, str(local_path))
|
||||
except (IOError, OSError) as exc:
|
||||
QMessageBox.critical(self, "Erreur", f"Telechargement impossible : {exc}")
|
||||
return
|
||||
|
||||
self._edited[str(local_path)] = remote_path
|
||||
self._watcher.addPath(str(local_path))
|
||||
self.status.setText(f"Edition de {name} — enregistrez pour renvoyer sur le serveur")
|
||||
try:
|
||||
os.startfile(str(local_path)) # ouvre avec l'application associee
|
||||
except OSError as exc:
|
||||
QMessageBox.warning(
|
||||
self, "Ouverture impossible",
|
||||
f"Le fichier a ete telecharge dans :\n{local_path}\n\n"
|
||||
f"Ouvrez-le manuellement ; il sera renvoye a chaque enregistrement.\n({exc})",
|
||||
)
|
||||
|
||||
def _on_edited_file_changed(self, local_path: str) -> None:
|
||||
remote_path = self._edited.get(local_path)
|
||||
if not remote_path:
|
||||
return
|
||||
try:
|
||||
self.sftp.put(local_path, remote_path)
|
||||
except (IOError, OSError) as exc:
|
||||
self.status.setText(f"Echec du renvoi : {exc}")
|
||||
return
|
||||
# Beaucoup d'editeurs enregistrent en remplacant le fichier : le chemin surveille
|
||||
# disparait alors du watcher, il faut le reinscrire pour les enregistrements suivants.
|
||||
if local_path not in self._watcher.files() and Path(local_path).exists():
|
||||
self._watcher.addPath(local_path)
|
||||
self.refresh()
|
||||
# apres refresh() : sinon le message serait ecrase par le compteur d'elements
|
||||
self.status.setText(f"↑ {posixpath.basename(remote_path)} renvoye sur le serveur")
|
||||
|
||||
def change_permissions(self) -> None:
|
||||
entries = self.selected_entries()
|
||||
if not entries:
|
||||
|
||||
Reference in New Issue
Block a user