feat: Add automatic backup to Google Drive and local disk - #3138
Conversation
- גיבוי אוטומטי ל-Google Drive עם OAuth redirect flow מהוובאפ - גיבוי אוטומטי לדיסק (/var/data/repos/backups) עם ניקוי retention - Scheduler thread ברקע שסורק כל 5 דקות ומריץ גיבויים מתוזמנים - שחזור אסינכרוני עם progress bar ו-batching (מנות של 20) - תיקון אינדקס כפול ב-bookmarks (file_bookmarks_user_file_idx) - UI חדש בהגדרות: סקשנים ל-Drive, דיסק, ותדירות גיבוי https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
🧯 Dangerous deletes guard reportPolicy: see .cursorrules — dangerous deletions are blocked unless wrapped safely. Summary:
Flagged findings (file:line:snippet): Excluded matches (by path pattern) |
⏱️ Performance report(No performance test durations collected. Mark tests with |
| base = os.getenv("WEBAPP_BASE_URL", "").rstrip("/") | ||
| if not base: | ||
| base = request.url_root.rstrip("/") | ||
| return redirect(f"{base}/settings?{query}#backup-section") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
General approach: Ensure that redirects never use an attacker-controlled host or scheme. Keep user input restricted to inert parts of the URL (like query parameters) and derive the redirect base from a trusted configuration value or from the current app context in a way that cannot be overridden by tampered headers.
Best concrete fix here:
- Make
_settings_redirectbuild the target URL usingurl_forwith_external=Trueso that the host/scheme come from Flask’s configuration rather than directly fromrequest.url_rootor environment variables that might be mis-set. - Keep
querystrictly as part of the query string and do not allow it to alter the base path or host. - To avoid changing external behavior, still redirect to
/settingswith#backup-sectionand arbitrary query parameters passed in asquery.
A minimal change that achieves this:
- Replace manual string concatenation of
baseand path with a call tourl_for('settings', _external=True)(or if a specific endpoint name is not known, at least construct a relative path instead of absolute withrequest.url_root). However, we are constrained not to assume endpoints outside the snippet, so we should avoid introducingurl_for('settings', ...)if we don't know that endpoint exists. - Instead, we can:
- Always use a relative redirect path (
/settings?...#backup-section) rather than a fully-qualified URL with host and scheme. Relative redirects are resolved by the browser against the current origin, so they can't redirect off-site. - Keep using
WEBAPP_URLelsewhere if needed, but for_settings_redirect, we returnredirect(f"/settings?{query}#backup-section").
- Always use a relative redirect path (
This preserves the visible behavior (path /settings, same query, fragment #backup-section) but removes any dependence of the redirect URL on request.url_root or environment-based host, eliminating any open-redirect risk. No new imports are needed.
Concretely, in webapp/drive_auth.py, update _settings_redirect:
- Remove the logic building
basefromWEBAPP_URLorrequest.url_root. - Return
redirect(f"/settings?{query}#backup-section").
| @@ -249,7 +249,6 @@ | ||
|
|
||
| def _settings_redirect(query: str) -> Response: | ||
| """Redirect back to settings page with query params.""" | ||
| base = os.getenv("WEBAPP_URL", "").rstrip("/") | ||
| if not base: | ||
| base = request.url_root.rstrip("/") | ||
| return redirect(f"{base}/settings?{query}#backup-section") | ||
| # Use a relative path so the redirect always stays on the same origin. | ||
| # The `query` argument is expected to be pre-encoded key=value pairs. | ||
| return redirect(f"/settings?{query}#backup-section") |
📖 Documentation PreviewThe documentation has been built successfully!
To view locally:
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…leanup - Race condition ב-restore_progress: snapshot כל השדות + pop בתוך אותו lock - Disk backup: עדכון last_backup_at רק בהצלחה (ok=True) - דליפת זיכרון: cleanup entries ישנים מעל שעה ב-_active_restores https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
מעביר את בדיקת user_id לפני ה-pop מ-_active_restores כדי שמשתמש לא-מורשה לא ימחק entry של משתמש אחר. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
ה-before_request handler הגביל גודל רק ל-restore הסינכרוני. עכשיו מכסה גם את restore_backup_async שדרכו עובר כל ה-traffic. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
- drive_auth: כל ערכי error ב-redirects עוברים _url_encode למניעת URL שבור - backup_api: תוצאת restore לא נמחקת מיד אלא מסומנת fetched_at, ונמחקת רק אחרי 5 דקות grace — כך שאם ה-response אבד ברשת הלקוח יכול לקרוא שוב בלי לקבל 404 https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
_cleanup_stale_restores היה מוחק entries שעבר שעה מאז יצירתם, בלי לבדוק אם הם עדיין בסטטוס running. עכשיו entries שרצים לא נמחקים ע"י age check — רק entries שסיימו (done/error). https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
1. backup_api — העברת _active_restores מ-dict בזיכרון ל-collection restore_jobs ב-MongoDB. פותר: - High: state לא אבד ב-restart או בין workers - Low: stuck running entries נמחקים ע"י TTL index (שעה) 2. backup_scheduler — _scan_and_run משתמש ב-find_one_and_update לתפיסה אטומית של משתמשים. רק scheduler אחד מריץ גיבוי לכל משתמש, גם עם N gunicorn workers. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
אם uid או schedule_key חסרים אחרי ה-claim האטומי, מחזיר את schedule_next_at לערך המקורי כדי למנוע תקיעה ב-2099. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
כשגיבוי נכשל, schedule_next_at נדחה ב-SCAN_INTERVAL קדימה (5 דקות) במקום להחזיר את הערך המקורי שעדיין $lte now — מה שגרם ל-while True לתפוס את אותו משתמש שוב מיד (לולאה אינסופית). אותו תיקון ב-_reset_drive_schedule ו-_reset_disk_schedule. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
…handling
באג 1 (High): כיבוי schedule ל-Drive ("off") לא עצר את ה-scheduler
כי save_drive_prefs עושה merge ושדות legacy כמו schedule: "daily" נשארים.
תיקון defense-in-depth ב-3 שכבות:
- $unset שדות legacy בכיבוי (drive_backup_api.py)
- $ne "off" + $ne None בשאילתת הסריקה (backup_scheduler.py)
- early return ב-_extract_schedule_key כש-schedule_key=="off"
באג 2 (Low): _complete_restore_job חסר error handling.
אם הקריאה נכשלת ב-success path, היא נופלת ל-except ומדווחת
שחזור מוצלח כנכשל. תיקון:
- try/except ב-_complete_restore_job
- הפרדת תוצאת השחזור מהעדכון ל-DB
https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
…sert, null guard)
1. drive_auth.py: _settings_redirect return type -> Response (לא str)
2. backup_api.py: בדיקת result.get("ok") is False לפני סימון "done"
כדי לא לדווח שחזור כושל כהצלחה
3. drive_backup_api.py: הסרת upsert=True ב-set_disk_schedule
כדי לא ליצור מסמך user חלקי
4. backup_scheduler.py: הוספת $ne None לשאילתת disk schedule_next_at
בהתאמה לתיקון שנעשה בשאילתת Drive
https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
1. backup_scheduler.py: _extract_schedule_key — val.get("key") or val.get("value")
גורם ל-short-circuit כש-key הוא string לא-ריק אבל לא תקין,
ו-value לעולם לא נבדק. הוחלף בלולאה על שני השדות בנפרד.
2. settings.html: לולאת polling של restore ללא timeout —
אם ה-job תקוע ב-running, הדפדפן סובב עד שעה.
הוסף guard של 10 דקות (300 סיבובים × 2 שניות).
https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
1. backup_scheduler.py: trigger_drive_backup_now ו-trigger_disk_backup_now לא עדכנו last_backup_at — ה-UI הציג תאריך ישן אחרי "גבה עכשיו". הוסף עדכון $set אחרי הצלחה. 2. drive_backup_api.py: save_drive_prefs עושה read-modify-write שיכול לדרוס sentinel של ה-scheduler. הוחלף ב-$set ישיר על שדות ספציפיים (dot notation), כמו שכבר עושים ב-set_disk_schedule. $unset ל-legacy fields שולב באותו update_one. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
_cleanup_disk_backups השתמש בלוגיקת OR — גיבוי נמחק אם עבר max *או* אם עבר retention. כך משתמש שלא גיבה 30+ יום איבד את כל הגיבויים הקיימים (חוץ מהאחרון). שונה ל: ה-N הכי חדשים (max) נשמרים תמיד. מחיקה חלה רק על גיבויים מעבר ל-max שגם עברו את ה-retention. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
…erwrite drive_callback השתמש ב-save_drive_prefs (read-modify-write) לשמירת drive_email, מה שיכול לדרוס את ה-sentinel של ה-scheduler. הוחלף ב-$set ישיר על drive_prefs.drive_email — אותו pattern שכבר תוקן ב-set_drive_schedule. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
1. drive_auth.py: drive_callback השתמש ב-@_require_auth שמחזיר JSON 401 — בדפדפן (redirect מ-Google) המשתמש רואה JSON גולמי. הוחלף בבדיקת session ידנית עם redirect לדף הגדרות + הודעת שגיאה. 2. backup_api.py: _complete_restore_job איפס progress ל-0 בשגיאה, מה שגורם לקפיצה אחורה בפרוגרס בר (65% → 0%). בשגיאה, progress לא נדרס — נשמר הערך האחרון. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
1. get_disk_backup_info: total_size חישב רק את 20 הראשונים אבל count החזיר את כולם — גודל מטעה ב-UI. עכשיו total_size סוכם את כל הקבצים. 2. sentinel orphan recovery: אם ה-process קרס בין claim לעדכון, ה-sentinel "2099-01-01" נשאר לנצח והגיבוי תקוע. הוסף _recover_orphaned_sentinels שרץ בתחילת כל סריקה — מאפס sentinels ל-retry_next_at. הוצא הערך הקשיח לקבוע _SENTINEL_VALUE. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
1. sentinel: הוחלף מערך קבוע "2099" ל-now + 30 דקות (SENTINEL_TTL).
- אם ה-backup מסתיים בזמן — הערך מוחלף כרגיל
- אם ה-process קרס — אחרי 30 דקות הערך פוקע ($lte now)
ונתפס מחדש טבעית
- הוסר _recover_orphaned_sentinels שאיפס גם backups פעילים
של workers אחרים
2. trigger_drive_backup_now: הוסר עדכון כפול של last_backup_at.
perform_scheduled_backup כבר מעדכן דרך save_drive_prefs —
עדכון שני ב-$set גרם ל-race condition.
https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
_extract_schedule_key בscheduler היה כפילות של extract_schedule_key מ-handlers/drive/utils.py עם סדר שדות שונה ותמיכה חלקית (חסר scheduleKey, name). עכשיו משתמש בפונקציה המשותפת עם ולידציה כנגד SCHEDULE_INTERVALS ובדיקת "off". https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
האינדקס (user_id, file_id) הוחלף מ-user_file_lookup
ל-file_bookmarks_user_file_idx, אבל הישן לא נמחק.
MongoDB לא מוחק אינדקס ישן אוטומטית כשיוצרים חדש
באותם שדות עם שם אחר — נשארים שני אינדקסים.
הוסף drop_index("user_file_lookup") ליד ה-drop הקיים.
https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
_extract_schedule_key תפס ImportError והחזיר None תמיד, מה שגרם ל-scheduler לתפוס משתמשים ולאפס מיד (sentinel reset) בלי להריץ גיבוי — שקט ולנצח. הוסף fallback מקומי שבודק schedule_key, scheduleKey, schedule ישירות מ-drive_prefs כשה-import נכשל. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
_extract_schedule_key נקרא מחוץ ל-try/except בלולאת הסריקה. אם זורק exception (למשל מתוך extract_schedule_key המיובא), כל הלולאה נעצרת — ה-sentinel לא מאופס והמשתמשים הנותרים נדלגים. תיקון: - עטיפה ב-try/except סביב _extract_schedule_key בלולאת הסריקה - שינוי except ImportError ל-except Exception בפונקציה עצמה כך שגם שגיאות מתוך extract_schedule_key נתפסות https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
1. backup_api.py: restore_backup_async לא בדק אם כבר רץ restore לאותו user. שתי קריאות מקבילות גורמות לכתיבות כפולות ל-DB. הוסף בדיקת status: "running" לפני יצירת job חדש (409). 2. backup_scheduler.py: _scan_drive_backups הוא while True שרץ לפני _scan_disk_backups. אחרי השבתה ארוכה, הרבה drive backups בתור גורמים ל-disk להירעב שעות. הוסף MAX_BACKUPS_PER_SCAN (10) — כל סוג מעבד עד 10 בסריקה, והנותרים ייתפסו בסריקה הבאה. https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5

✨ תיאור קצר
הוספת מערכת גיבויים אוטומטיים מלאה לוובאפ:
📦 שינויים עיקריים
Backend
webapp/backup_scheduler.py(חדש): Daemon thread שמנהל גיבויים אוטומטייםwebapp/drive_auth.py(חדש): Google Drive OAuth flow/api/drive/auth- הפניה ל-Google consent screen/api/drive/callback- קבלת authorization code והחלפה לטוקנים/api/drive/status- בדיקת סטטוס חיבור/api/drive/disconnect- ניתוק Drivewebapp/drive_backup_api.py(חדש): API endpoints לניהול גיבויים/api/drive/schedule- הגדרת תדירות גיבוי ל-Drive/api/drive/backup-now- גיבוי מיידי ל-Drive/api/disk-backup/schedule- הגדרת תדירות גיבוי לדיסק/api/disk-backup/now- גיבוי מיידי לדיסק/api/disk-backup/status- סטטוס גיבויים לדיסקwebapp/backup_api.py(שונה):/api/backup/restore-async- שחזור אסינכרוני עם דיווח התקדמות/api/backup/restore-progress/<id>- polling על מצב השחזורservices/personal_backup_service.py(שונה):progress_cbcallback ל-restore_user_data()לדיווח התקדמותwebapp/app.py(שונה):drive_auth_bpו-drive_backup_bpFrontend
webapp/templates/settings.html(שונה):https://claude.ai/code/session_019psqKqV6MhkHQRkqt8Szz5