Extended document redaction to Google Drive, SFTP, SMB, and local PDFs Extends the ✂ in-place redaction feature beyond local DOCX/XLSX/CSV/TXT files to cover all remaining file source types and adds PDF support for local files.

This commit is contained in:
StyxX65 committed 2026-05-28 17:47:02 +02:00
1 parent 6ce7583b26
commit 034ced943e
11 files changed
+723 -69

No files matched your search

+67
View File
@@ -70,6 +70,9 @@ GMAIL_SCOPES = [
DRIVE_SCOPES = [
"https://www.googleapis.com/auth/drive.readonly",
]
DRIVE_WRITE_SCOPES = [
"https://www.googleapis.com/auth/drive",
]
ADMIN_SCOPES = [
"https://www.googleapis.com/auth/admin.directory.user.readonly",
]
@@ -284,6 +287,26 @@ class GoogleConnector:
raise GoogleError(f"Drive auth failed for {user_email}: {e}") from e
return _drive_changes_collect(service, user_email, page_token, max_files, max_file_mb)
# ── Drive write-back (redaction) ──────────────────────────────────────────
def get_drive_file_mime(self, user_email: str, file_id: str) -> str:
"""Return the mimeType of a Drive file."""
creds = self._creds_for(user_email, DRIVE_WRITE_SCOPES)
service = build("drive", "v3", credentials=creds, cache_discovery=False)
return _get_drive_file_mime(service, file_id)
def download_drive_file_by_id(self, user_email: str, file_id: str) -> bytes:
"""Download raw bytes of a non-Google-native Drive file by ID."""
creds = self._creds_for(user_email, DRIVE_WRITE_SCOPES)
service = build("drive", "v3", credentials=creds, cache_discovery=False)
return _download_drive_file_by_id(service, file_id)
def update_drive_file(self, user_email: str, file_id: str, content: bytes, mime_type: str) -> None:
"""Replace Drive file content in-place. Requires drive (not drive.readonly) scope."""
creds = self._creds_for(user_email, DRIVE_WRITE_SCOPES)
service = build("drive", "v3", credentials=creds, cache_discovery=False)
_update_drive_file_content(service, file_id, content, mime_type)
# ── Persistence helpers ───────────────────────────────────────────────────────
@@ -507,6 +530,30 @@ def _download_drive_file(
return None
def _get_drive_file_mime(service, file_id: str) -> str:
"""Return the mimeType of a Drive file."""
info = service.files().get(fileId=file_id, fields="mimeType").execute()
return info.get("mimeType", "")
def _download_drive_file_by_id(service, file_id: str) -> bytes:
"""Download raw bytes of a non-Google-native Drive file by ID."""
req = service.files().get_media(fileId=file_id)
buf = io.BytesIO()
dl = MediaIoBaseDownload(buf, req, chunksize=4 * 1024 * 1024)
done = False
while not done:
_, done = dl.next_chunk()
return buf.getvalue()
def _update_drive_file_content(service, file_id: str, content: bytes, mime_type: str) -> None:
"""Replace a Drive file's content in-place."""
from googleapiclient.http import MediaInMemoryUpload
media = MediaInMemoryUpload(content, mimetype=mime_type, resumable=False)
service.files().update(fileId=file_id, media_body=media).execute()
def _drive_iter(
service,
user_email: str,
@@ -743,6 +790,26 @@ class PersonalGoogleConnector:
raise GoogleError(f"Drive auth failed: {e}") from e
return _drive_changes_collect(service, user_email, page_token, max_files, max_file_mb)
# ── Drive write-back (redaction) ──────────────────────────────────────────
def get_drive_file_mime(self, user_email: str, file_id: str) -> str:
"""Return the mimeType of a Drive file."""
self._refresh_if_needed()
service = build("drive", "v3", credentials=self._creds, cache_discovery=False)
return _get_drive_file_mime(service, file_id)
def download_drive_file_by_id(self, user_email: str, file_id: str) -> bytes:
"""Download raw bytes of a non-Google-native Drive file by ID."""
self._refresh_if_needed()
service = build("drive", "v3", credentials=self._creds, cache_discovery=False)
return _download_drive_file_by_id(service, file_id)
def update_drive_file(self, user_email: str, file_id: str, content: bytes, mime_type: str) -> None:
"""Replace Drive file content in-place. Requires drive (not drive.readonly) scope."""
self._refresh_if_needed()
service = build("drive", "v3", credentials=self._creds, cache_discovery=False)
_update_drive_file_content(service, file_id, content, mime_type)
@staticmethod
def get_device_code_flow(client_id: str, client_secret: str) -> dict:
"""