From e773b19508711569928549f355894286a95d810e Mon Sep 17 00:00:00 2001 From: byrsapty Date: Sat, 5 Sep 2026 23:26:38 +0300 Subject: [PATCH] OAuth Google Drive: use a plain folder path instead of a folder ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A real OAuth-authorized account has its own "My Drive" with actual storage, so unlike the service-account path (which has none of its own and can only write into an explicitly shared-by-ID folder), there's no need to hunt down a numeric folder ID — a plain path segment works and rclone creates it automatically on first upload if it doesn't exist yet. Co-Authored-By: Claude Sonnet 5 --- bober_bbq/admin/system.py | 2 ++ bober_bbq/models.py | 1 + bober_bbq/templates/admin/backups.html | 13 ++++++----- bober_bbq/utils/backup_remote.py | 31 ++++++++++++++++---------- tests/test_backup_remote.py | 14 ++++++++++++ 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/bober_bbq/admin/system.py b/bober_bbq/admin/system.py index 641a78b..49fd6fd 100644 --- a/bober_bbq/admin/system.py +++ b/bober_bbq/admin/system.py @@ -112,6 +112,7 @@ def backups_list(): "backup_gdrive_client_secret", "backup_gdrive_scope", "backup_gdrive_token", + "backup_gdrive_folder_path", "backup_s3_provider", "backup_s3_access_key_id", "backup_s3_secret_access_key", @@ -146,6 +147,7 @@ def backups_remote_settings(): Setting.set("backup_gdrive_client_secret", request.form.get("backup_gdrive_client_secret", "").strip()) Setting.set("backup_gdrive_scope", request.form.get("backup_gdrive_scope", "").strip()) Setting.set("backup_gdrive_token", request.form.get("backup_gdrive_token", "").strip()) + Setting.set("backup_gdrive_folder_path", request.form.get("backup_gdrive_folder_path", "").strip()) Setting.set("backup_s3_provider", request.form.get("backup_s3_provider", "AWS").strip() or "AWS") Setting.set("backup_s3_access_key_id", request.form.get("backup_s3_access_key_id", "").strip()) Setting.set("backup_s3_secret_access_key", request.form.get("backup_s3_secret_access_key", "").strip()) diff --git a/bober_bbq/models.py b/bober_bbq/models.py index 3855d04..3fd92ff 100644 --- a/bober_bbq/models.py +++ b/bober_bbq/models.py @@ -639,6 +639,7 @@ DEFAULT_SETTINGS = { "backup_gdrive_client_secret": "", "backup_gdrive_scope": "", "backup_gdrive_token": "", + "backup_gdrive_folder_path": "", "backup_s3_provider": "AWS", "backup_s3_access_key_id": "", "backup_s3_secret_access_key": "", diff --git a/bober_bbq/templates/admin/backups.html b/bober_bbq/templates/admin/backups.html index 36574b6..fdf248d 100644 --- a/bober_bbq/templates/admin/backups.html +++ b/bober_bbq/templates/admin/backups.html @@ -78,14 +78,14 @@
- - - -

Заповніть або «Сервісний акаунт», або «OAuth» нижче — не обидва одразу.

+

Заповніть або «Сервісний акаунт», або «OAuth» нижче — не обидва одразу.

+ + +
Сервісний акаунт — як налаштувати (крок за кроком)
    @@ -116,13 +116,16 @@ + + +
    OAuth — як налаштувати (крок за кроком)
    1. Встановіть rclone на своєму комп'ютері (не на сервері) — rclone.org/downloads.
    2. Запустіть rclone config, оберіть «New remote» → тип drive, погодьтесь на автентифікацію через браузер (увійдіть у свій Google-акаунт).
    3. Коли готово, відкрийте створений конфіг (шлях покаже сама команда, зазвичай ~/.config/rclone/rclone.conf) і скопіюйте значення client_id, client_secret, scope, token у поля вище.
    4. -
    5. ID папки — необов'язково для OAuth (за замовчуванням доступний весь «Мій диск» акаунта, з яким ви увійшли); заповніть, якщо хочете обмежити конкретною папкою.
    6. +
    7. «Шлях до папки» — звичайна назва (не ID); можна лишити порожнім, і бекапи ляжуть у корінь «Мій диск» того акаунта, з яким ви увійшли.
    8. Збережіть, натисніть «Перевірити підключення».
    diff --git a/bober_bbq/utils/backup_remote.py b/bober_bbq/utils/backup_remote.py index c6b67da..421d4fd 100644 --- a/bober_bbq/utils/backup_remote.py +++ b/bober_bbq/utils/backup_remote.py @@ -77,7 +77,6 @@ def _remote_env_and_path() -> tuple[dict, str]: service_account_json = Setting.get("backup_gdrive_service_account_json", "").strip() client_id = Setting.get("backup_gdrive_client_id", "").strip() token = Setting.get("backup_gdrive_token", "").strip() - folder_id = Setting.get("backup_gdrive_folder_id", "").strip() env[prefix + "TYPE"] = "drive" if service_account_json: @@ -85,14 +84,19 @@ def _remote_env_and_path() -> tuple[dict, str]: json.loads(service_account_json) except json.JSONDecodeError as e: raise BackupError(f"Невалідний JSON ключа сервісного акаунта: {e}") from e - env[prefix + "SERVICE_ACCOUNT_CREDENTIALS"] = service_account_json + folder_id = Setting.get("backup_gdrive_folder_id", "").strip() if not folder_id: # A service account has no Drive storage of its own — it # can only write into a folder that's been explicitly - # shared with it, so this isn't optional the way it is for - # a real user's OAuth-authorized "My Drive" below. + # shared with it (root_folder_id is a backend option, not + # part of the path, so rclone can't just "create" this the + # way it can for a real OAuth-authorized account below). raise BackupError("Для сервісного акаунта обов'язково вкажіть ID папки Google Drive") - elif client_id and token: + env[prefix + "SERVICE_ACCOUNT_CREDENTIALS"] = service_account_json + env[prefix + "ROOT_FOLDER_ID"] = folder_id + return env, f"{_REMOTE}:" + + if client_id and token: try: json.loads(token) except json.JSONDecodeError as e: @@ -105,13 +109,16 @@ def _remote_env_and_path() -> tuple[dict, str]: if scope: env[prefix + "SCOPE"] = scope env[prefix + "TOKEN"] = token - else: - raise BackupError( - "Вкажіть або ключ сервісного акаунта, або client_id + token (OAuth) для Google Drive" - ) - if folder_id: - env[prefix + "ROOT_FOLDER_ID"] = folder_id - return env, f"{_REMOTE}:" + # A real OAuth-authorized account has its own "My Drive" with + # real storage, so — unlike the service-account path above — + # there's no numeric folder id to hunt down: a plain path + # segment is enough, and rclone creates it on first upload if + # it doesn't exist yet. + folder_path = Setting.get("backup_gdrive_folder_path", "").strip().strip("/") + remote_path = f"{_REMOTE}:{folder_path}" if folder_path else f"{_REMOTE}:" + return env, remote_path + + raise BackupError("Вкажіть або ключ сервісного акаунта, або client_id + token (OAuth) для Google Drive") if remote_type == "s3": access_key_id = Setting.get("backup_s3_access_key_id", "").strip() diff --git a/tests/test_backup_remote.py b/tests/test_backup_remote.py index 2177f3d..9ba49a1 100644 --- a/tests/test_backup_remote.py +++ b/tests/test_backup_remote.py @@ -77,6 +77,20 @@ def test_gdrive_oauth_env_and_path(app): assert path == "backup:" +def test_gdrive_oauth_folder_path_is_a_plain_path_not_an_id(app): + with app.app_context(): + Setting.set("backup_remote_type", "gdrive") + Setting.set("backup_gdrive_service_account_json", "") + Setting.set("backup_gdrive_client_id", "CLIENT123") + Setting.set("backup_gdrive_token", '{"access_token": "a"}') + Setting.set("backup_gdrive_folder_path", "/Бекапи/BoberBBQ/") + db.session.commit() + + env, path = backup_remote._remote_env_and_path() + assert "RCLONE_CONFIG_BACKUP_ROOT_FOLDER_ID" not in env + assert path == "backup:Бекапи/BoberBBQ" + + def test_gdrive_neither_method_configured_raises(app): with app.app_context(): Setting.set("backup_remote_type", "gdrive")