machineconfig 1.97__py3-none-any.whl → 2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/cluster/cloud_manager.py +22 -29
- machineconfig/cluster/data_transfer.py +2 -3
- machineconfig/cluster/distribute.py +0 -2
- machineconfig/cluster/file_manager.py +4 -5
- machineconfig/cluster/job_params.py +1 -4
- machineconfig/cluster/loader_runner.py +8 -11
- machineconfig/cluster/remote_machine.py +4 -5
- machineconfig/cluster/script_execution.py +2 -2
- machineconfig/cluster/script_notify_upon_completion.py +0 -1
- machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +4 -6
- machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -1
- machineconfig/cluster/sessions_managers/enhanced_command_runner.py +35 -75
- machineconfig/cluster/sessions_managers/wt_local.py +113 -185
- machineconfig/cluster/sessions_managers/wt_local_manager.py +127 -197
- machineconfig/cluster/sessions_managers/wt_remote.py +60 -67
- machineconfig/cluster/sessions_managers/wt_remote_manager.py +110 -149
- machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +61 -64
- machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +72 -172
- machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +27 -60
- machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +58 -137
- machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +46 -74
- machineconfig/cluster/sessions_managers/zellij_local.py +91 -147
- machineconfig/cluster/sessions_managers/zellij_local_manager.py +165 -190
- machineconfig/cluster/sessions_managers/zellij_remote.py +51 -58
- machineconfig/cluster/sessions_managers/zellij_remote_manager.py +40 -46
- machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +19 -17
- machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +30 -31
- machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +64 -134
- machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +7 -11
- machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +27 -55
- machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +14 -13
- machineconfig/cluster/templates/cli_click.py +0 -1
- machineconfig/cluster/templates/cli_gooey.py +0 -2
- machineconfig/cluster/templates/cli_trogon.py +0 -1
- machineconfig/cluster/templates/run_cloud.py +0 -1
- machineconfig/cluster/templates/run_cluster.py +0 -1
- machineconfig/cluster/templates/run_remote.py +0 -1
- machineconfig/cluster/templates/utils.py +27 -11
- machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/linux/msc/cli_agents.sh +16 -0
- machineconfig/jobs/python/check_installations.py +9 -9
- machineconfig/jobs/python/create_bootable_media.py +0 -2
- machineconfig/jobs/python/python_cargo_build_share.py +2 -2
- machineconfig/jobs/python/python_ve_symlink.py +9 -11
- machineconfig/jobs/python/tasks.py +0 -1
- machineconfig/jobs/python/vscode/api.py +5 -5
- machineconfig/jobs/python/vscode/link_ve.py +20 -21
- machineconfig/jobs/python/vscode/select_interpreter.py +28 -29
- machineconfig/jobs/python/vscode/sync_code.py +14 -18
- machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_custom_installers/archive/ngrok.py +15 -15
- machineconfig/jobs/python_custom_installers/dev/aider.py +10 -18
- machineconfig/jobs/python_custom_installers/dev/alacritty.py +12 -21
- machineconfig/jobs/python_custom_installers/dev/brave.py +13 -22
- machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +13 -20
- machineconfig/jobs/python_custom_installers/dev/code.py +17 -24
- machineconfig/jobs/python_custom_installers/dev/cursor.py +10 -21
- machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +12 -11
- machineconfig/jobs/python_custom_installers/dev/espanso.py +19 -23
- machineconfig/jobs/python_custom_installers/dev/goes.py +9 -16
- machineconfig/jobs/python_custom_installers/dev/lvim.py +13 -21
- machineconfig/jobs/python_custom_installers/dev/nerdfont.py +15 -22
- machineconfig/jobs/python_custom_installers/dev/redis.py +15 -23
- machineconfig/jobs/python_custom_installers/dev/wezterm.py +15 -22
- machineconfig/jobs/python_custom_installers/dev/winget.py +32 -50
- machineconfig/jobs/python_custom_installers/docker.py +15 -24
- machineconfig/jobs/python_custom_installers/gh.py +18 -26
- machineconfig/jobs/python_custom_installers/hx.py +33 -17
- machineconfig/jobs/python_custom_installers/warp-cli.py +15 -23
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_generic_installers/config.json +412 -389
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_windows_installers/dev/config.json +1 -1
- machineconfig/jobs/windows/archive/archive_pygraphviz.ps1 +1 -1
- machineconfig/jobs/windows/msc/cli_agents.bat +0 -0
- machineconfig/jobs/windows/msc/cli_agents.ps1 +0 -0
- machineconfig/jobs/windows/start_terminal.ps1 +1 -1
- machineconfig/logger.py +50 -0
- machineconfig/profile/create.py +50 -36
- machineconfig/profile/create_hardlinks.py +33 -26
- machineconfig/profile/shell.py +87 -60
- machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/cloud/init.sh +2 -2
- machineconfig/scripts/linux/checkout_versions +1 -1
- machineconfig/scripts/linux/choose_wezterm_theme +1 -1
- machineconfig/scripts/linux/cloud_copy +1 -1
- machineconfig/scripts/linux/cloud_manager +1 -1
- machineconfig/scripts/linux/cloud_mount +1 -1
- machineconfig/scripts/linux/cloud_repo_sync +1 -1
- machineconfig/scripts/linux/cloud_sync +1 -1
- machineconfig/scripts/linux/croshell +1 -1
- machineconfig/scripts/linux/devops +3 -5
- machineconfig/scripts/linux/fire +2 -1
- machineconfig/scripts/linux/fire_agents +3 -3
- machineconfig/scripts/linux/ftpx +1 -1
- machineconfig/scripts/linux/gh_models +1 -1
- machineconfig/scripts/linux/kill_process +1 -1
- machineconfig/scripts/linux/mcinit +2 -2
- machineconfig/scripts/linux/repos +1 -1
- machineconfig/scripts/linux/scheduler +1 -1
- machineconfig/scripts/linux/start_slidev +1 -1
- machineconfig/scripts/linux/start_terminals +1 -1
- machineconfig/scripts/linux/url2md +1 -1
- machineconfig/scripts/linux/warp-cli.sh +122 -0
- machineconfig/scripts/linux/wifi_conn +1 -1
- machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__init__.py +0 -0
- machineconfig/scripts/python/ai/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/generate_files.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/chatmodes/Thinking-Beast-Mode.chatmode.md +337 -0
- machineconfig/scripts/python/ai/chatmodes/Ultimate-Transparent-Thinking-Beast-Mode.chatmode.md +644 -0
- machineconfig/scripts/python/ai/chatmodes/deepResearch.chatmode.md +81 -0
- machineconfig/scripts/python/ai/configs/.gemini/settings.json +81 -0
- machineconfig/scripts/python/ai/generate_files.py +84 -0
- machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +45 -0
- machineconfig/scripts/python/ai/mcinit.py +107 -0
- machineconfig/scripts/python/ai/prompts/allLintersAndTypeCheckers.prompt.md +5 -0
- machineconfig/scripts/python/ai/prompts/research-report-skeleton.prompt.md +38 -0
- machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +52 -0
- machineconfig/scripts/python/archive/tmate_conn.py +5 -5
- machineconfig/scripts/python/archive/tmate_start.py +3 -3
- machineconfig/scripts/python/choose_wezterm_theme.py +2 -2
- machineconfig/scripts/python/cloud_copy.py +20 -19
- machineconfig/scripts/python/cloud_mount.py +10 -8
- machineconfig/scripts/python/cloud_repo_sync.py +15 -15
- machineconfig/scripts/python/cloud_sync.py +1 -1
- machineconfig/scripts/python/croshell.py +18 -16
- machineconfig/scripts/python/devops.py +6 -6
- machineconfig/scripts/python/devops_add_identity.py +9 -7
- machineconfig/scripts/python/devops_add_ssh_key.py +19 -19
- machineconfig/scripts/python/devops_backup_retrieve.py +14 -14
- machineconfig/scripts/python/devops_devapps_install.py +3 -3
- machineconfig/scripts/python/devops_update_repos.py +141 -53
- machineconfig/scripts/python/dotfile.py +3 -3
- machineconfig/scripts/python/fire_agents.py +202 -41
- machineconfig/scripts/python/fire_jobs.py +20 -21
- machineconfig/scripts/python/ftpx.py +4 -3
- machineconfig/scripts/python/gh_models.py +94 -94
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/cloud_helpers.py +3 -3
- machineconfig/scripts/python/helpers/helpers2.py +3 -3
- machineconfig/scripts/python/helpers/helpers4.py +8 -7
- machineconfig/scripts/python/helpers/helpers5.py +7 -7
- machineconfig/scripts/python/helpers/repo_sync_helpers.py +2 -2
- machineconfig/scripts/python/mount_nfs.py +4 -3
- machineconfig/scripts/python/mount_nw_drive.py +4 -4
- machineconfig/scripts/python/mount_ssh.py +4 -3
- machineconfig/scripts/python/repos.py +9 -9
- machineconfig/scripts/python/scheduler.py +1 -1
- machineconfig/scripts/python/start_slidev.py +9 -8
- machineconfig/scripts/python/start_terminals.py +1 -1
- machineconfig/scripts/python/viewer.py +40 -40
- machineconfig/scripts/python/wifi_conn.py +65 -66
- machineconfig/scripts/python/wsl_windows_transfer.py +2 -2
- machineconfig/scripts/windows/checkout_version.ps1 +1 -3
- machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -3
- machineconfig/scripts/windows/cloud_copy.ps1 +2 -6
- machineconfig/scripts/windows/cloud_manager.ps1 +1 -1
- machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -2
- machineconfig/scripts/windows/cloud_sync.ps1 +2 -2
- machineconfig/scripts/windows/croshell.ps1 +2 -2
- machineconfig/scripts/windows/devops.ps1 +1 -4
- machineconfig/scripts/windows/dotfile.ps1 +1 -3
- machineconfig/scripts/windows/fire.ps1 +1 -1
- machineconfig/scripts/windows/ftpx.ps1 +2 -2
- machineconfig/scripts/windows/gpt.ps1 +1 -1
- machineconfig/scripts/windows/kill_process.ps1 +1 -2
- machineconfig/scripts/windows/mcinit.ps1 +2 -2
- machineconfig/scripts/windows/mount_nfs.ps1 +1 -1
- machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
- machineconfig/scripts/windows/pomodoro.ps1 +1 -1
- machineconfig/scripts/windows/py2exe.ps1 +1 -3
- machineconfig/scripts/windows/repos.ps1 +1 -1
- machineconfig/scripts/windows/scheduler.ps1 +1 -1
- machineconfig/scripts/windows/snapshot.ps1 +2 -2
- machineconfig/scripts/windows/start_slidev.ps1 +1 -1
- machineconfig/scripts/windows/start_terminals.ps1 +1 -1
- machineconfig/scripts/windows/wifi_conn.ps1 +1 -1
- machineconfig/scripts/windows/wsl_windows_transfer.ps1 +1 -3
- machineconfig/settings/lf/linux/lfrc +1 -1
- machineconfig/settings/linters/.ruff.toml +2 -2
- machineconfig/settings/linters/.ruff_cache/.gitignore +2 -0
- machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +1 -0
- machineconfig/settings/lvim/windows/archive/config_additional.lua +1 -1
- machineconfig/settings/shells/ipy/profiles/default/startup/playext.py +71 -71
- machineconfig/settings/shells/wt/settings.json +8 -8
- machineconfig/settings/svim/linux/init.toml +1 -1
- machineconfig/settings/svim/windows/init.toml +1 -1
- machineconfig/setup_linux/web_shortcuts/croshell.sh +0 -54
- machineconfig/setup_linux/web_shortcuts/interactive.sh +6 -6
- machineconfig/setup_linux/web_shortcuts/tmp.sh +2 -0
- machineconfig/setup_windows/web_shortcuts/all.ps1 +2 -2
- machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +1 -1
- machineconfig/setup_windows/web_shortcuts/croshell.ps1 +1 -1
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +5 -5
- machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1 +51 -15
- machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +75 -18
- machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +52 -42
- machineconfig/utils/ai/browser_user_wrapper.py +5 -5
- machineconfig/utils/ai/generate_file_checklist.py +19 -22
- machineconfig/utils/ai/url2md.py +5 -3
- machineconfig/utils/cloud/onedrive/setup_oauth.py +5 -4
- machineconfig/utils/cloud/onedrive/transaction.py +192 -227
- machineconfig/utils/code.py +71 -43
- machineconfig/utils/installer.py +77 -85
- machineconfig/utils/installer_utils/installer_abc.py +29 -17
- machineconfig/utils/installer_utils/installer_class.py +188 -83
- machineconfig/utils/io_save.py +3 -15
- machineconfig/utils/links.py +22 -11
- machineconfig/utils/notifications.py +197 -0
- machineconfig/utils/options.py +38 -25
- machineconfig/utils/path.py +18 -6
- machineconfig/utils/path_reduced.py +637 -316
- machineconfig/utils/procs.py +69 -63
- machineconfig/utils/scheduling.py +11 -13
- machineconfig/utils/ssh.py +351 -0
- machineconfig/utils/terminal.py +225 -0
- machineconfig/utils/utils.py +13 -12
- machineconfig/utils/utils2.py +43 -10
- machineconfig/utils/utils5.py +242 -46
- machineconfig/utils/ve.py +11 -6
- {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/METADATA +15 -9
- {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/RECORD +232 -235
- machineconfig/cluster/self_ssh.py +0 -57
- machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python/archive/python_tools.txt +0 -12
- machineconfig/jobs/python/vscode/__pycache__/select_interpreter.cpython-311.pyc +0 -0
- machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python_generic_installers/update.py +0 -3
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
- machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/linux/activate_ve +0 -87
- machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_backup_retrieve.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
- machineconfig/scripts/python/ai/init.py +0 -56
- machineconfig/scripts/python/ai/rules/python/dev.md +0 -31
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/windows/activate_ve.ps1 +0 -54
- {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/WHEEL +0 -0
- {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/top_level.txt +0 -0
|
@@ -15,12 +15,12 @@ Requirements:
|
|
|
15
15
|
pip install requests
|
|
16
16
|
|
|
17
17
|
Setup Options:
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
Option 1: Direct OAuth2 Setup (Recommended)
|
|
20
20
|
1. Run setup_oauth_authentication() for first-time setup
|
|
21
21
|
2. Follow the interactive prompts to authorize
|
|
22
22
|
3. Tokens will be automatically saved and refreshed
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
Option 2: Using existing rclone token
|
|
25
25
|
1. Update the RCLONE_TOKEN with your rclone token
|
|
26
26
|
2. Set DRIVE_ID from your rclone config
|
|
@@ -43,12 +43,14 @@ import json
|
|
|
43
43
|
|
|
44
44
|
def get_rclone_token(section: str):
|
|
45
45
|
import platform
|
|
46
|
+
|
|
46
47
|
if platform.system() == "Windows":
|
|
47
48
|
rclone_file_path = Path(os.getenv("APPDATA", "")) / "rclone" / "rclone.conf"
|
|
48
49
|
else:
|
|
49
50
|
rclone_file_path = Path.home() / ".config" / "rclone" / "rclone.conf"
|
|
50
51
|
if rclone_file_path.exists():
|
|
51
52
|
import configparser
|
|
53
|
+
|
|
52
54
|
config = configparser.ConfigParser()
|
|
53
55
|
config.read(rclone_file_path)
|
|
54
56
|
if section in config:
|
|
@@ -57,16 +59,18 @@ def get_rclone_token(section: str):
|
|
|
57
59
|
return dict(results)
|
|
58
60
|
return None
|
|
59
61
|
|
|
62
|
+
|
|
60
63
|
# Configuration - Will be loaded from rclone config
|
|
61
64
|
_cached_config = None
|
|
62
65
|
|
|
66
|
+
|
|
63
67
|
def get_config(section: str = "odp") -> dict[str, Any]:
|
|
64
68
|
"""
|
|
65
69
|
Get OneDrive configuration from rclone config.
|
|
66
|
-
|
|
70
|
+
|
|
67
71
|
Args:
|
|
68
72
|
section: The rclone config section name (default: "odp")
|
|
69
|
-
|
|
73
|
+
|
|
70
74
|
Returns:
|
|
71
75
|
Dictionary containing token, drive_id, and drive_type
|
|
72
76
|
"""
|
|
@@ -75,39 +79,40 @@ def get_config(section: str = "odp") -> dict[str, Any]:
|
|
|
75
79
|
rclone_config = get_rclone_token(section)
|
|
76
80
|
if not rclone_config:
|
|
77
81
|
raise Exception(f"Could not find rclone config section '{section}'. Please set up rclone first.")
|
|
78
|
-
|
|
82
|
+
|
|
79
83
|
# Parse the token from rclone config
|
|
80
84
|
token_str = rclone_config.get("token", "{}")
|
|
81
85
|
try:
|
|
82
86
|
token_data = json.loads(token_str)
|
|
83
87
|
except json.JSONDecodeError:
|
|
84
88
|
raise Exception(f"Invalid token format in rclone config section '{section}'")
|
|
85
|
-
|
|
86
|
-
_cached_config = {
|
|
87
|
-
|
|
88
|
-
"drive_id": rclone_config.get("drive_id"),
|
|
89
|
-
"drive_type": rclone_config.get("drive_type", "personal")
|
|
90
|
-
}
|
|
91
|
-
|
|
89
|
+
|
|
90
|
+
_cached_config = {"token": token_data, "drive_id": rclone_config.get("drive_id"), "drive_type": rclone_config.get("drive_type", "personal")}
|
|
91
|
+
|
|
92
92
|
return _cached_config
|
|
93
93
|
|
|
94
|
+
|
|
94
95
|
def get_token() -> dict[str, Any]:
|
|
95
96
|
"""Get the current token from rclone config."""
|
|
96
97
|
return get_config()["token"]
|
|
97
98
|
|
|
99
|
+
|
|
98
100
|
def get_drive_id():
|
|
99
101
|
"""Get the drive ID from rclone config."""
|
|
100
102
|
return get_config()["drive_id"]
|
|
101
103
|
|
|
104
|
+
|
|
102
105
|
def get_drive_type():
|
|
103
106
|
"""Get the drive type from rclone config."""
|
|
104
107
|
return get_config()["drive_type"]
|
|
105
108
|
|
|
109
|
+
|
|
106
110
|
def clear_config_cache():
|
|
107
111
|
"""Clear the cached config to force reload from rclone."""
|
|
108
112
|
global _cached_config
|
|
109
113
|
_cached_config = None
|
|
110
114
|
|
|
115
|
+
|
|
111
116
|
# OAuth2 Configuration - You'll need to set these up in Azure App Registration
|
|
112
117
|
CLIENT_ID = os.getenv("ONEDRIVE_CLIENT_ID", "your_client_id_here")
|
|
113
118
|
CLIENT_SECRET = os.getenv("ONEDRIVE_CLIENT_SECRET", "your_client_secret_here") # Optional for public clients
|
|
@@ -121,7 +126,7 @@ OAUTH_TOKEN_ENDPOINT = "https://login.microsoftonline.com/common/oauth2/v2.0/tok
|
|
|
121
126
|
def is_token_valid() -> bool:
|
|
122
127
|
"""
|
|
123
128
|
Check if the current rclone token is still valid.
|
|
124
|
-
|
|
129
|
+
|
|
125
130
|
Returns:
|
|
126
131
|
True if token is valid, False otherwise
|
|
127
132
|
"""
|
|
@@ -131,16 +136,16 @@ def is_token_valid() -> bool:
|
|
|
131
136
|
expiry_str = token.get("expiry")
|
|
132
137
|
if not expiry_str:
|
|
133
138
|
return False
|
|
134
|
-
|
|
139
|
+
|
|
135
140
|
# Remove timezone info for parsing (rclone format includes timezone)
|
|
136
|
-
if
|
|
137
|
-
expiry_str = expiry_str.split(
|
|
138
|
-
elif
|
|
139
|
-
expiry_str = expiry_str.replace(
|
|
140
|
-
|
|
141
|
+
if "+" in expiry_str:
|
|
142
|
+
expiry_str = expiry_str.split("+")[0]
|
|
143
|
+
elif "Z" in expiry_str:
|
|
144
|
+
expiry_str = expiry_str.replace("Z", "")
|
|
145
|
+
|
|
141
146
|
expiry_time = datetime.fromisoformat(expiry_str)
|
|
142
147
|
current_time = datetime.now()
|
|
143
|
-
|
|
148
|
+
|
|
144
149
|
# Add some buffer time (5 minutes)
|
|
145
150
|
return expiry_time > current_time + timedelta(minutes=5)
|
|
146
151
|
except Exception as e:
|
|
@@ -151,16 +156,16 @@ def is_token_valid() -> bool:
|
|
|
151
156
|
def get_access_token() -> Optional[str]:
|
|
152
157
|
"""
|
|
153
158
|
Get access token, automatically refreshing if expired.
|
|
154
|
-
|
|
159
|
+
|
|
155
160
|
Returns:
|
|
156
161
|
Access token string or None if token cannot be obtained/refreshed
|
|
157
162
|
"""
|
|
158
163
|
# First try to load token from file if it exists
|
|
159
164
|
load_token_from_file()
|
|
160
|
-
|
|
165
|
+
|
|
161
166
|
if not is_token_valid():
|
|
162
167
|
print("🔄 Access token has expired, attempting to refresh...")
|
|
163
|
-
|
|
168
|
+
|
|
164
169
|
# Try to refresh the token
|
|
165
170
|
refreshed_token = refresh_access_token()
|
|
166
171
|
if refreshed_token:
|
|
@@ -171,7 +176,7 @@ def get_access_token() -> Optional[str]:
|
|
|
171
176
|
print("1. Run setup_oauth_authentication() to set up OAuth")
|
|
172
177
|
print("2. Update your rclone token by running: rclone config reconnect odp")
|
|
173
178
|
return None
|
|
174
|
-
|
|
179
|
+
|
|
175
180
|
token = get_token()
|
|
176
181
|
return token.get("access_token")
|
|
177
182
|
|
|
@@ -179,72 +184,72 @@ def get_access_token() -> Optional[str]:
|
|
|
179
184
|
def make_graph_request(method: str, endpoint: str, **kwargs: Any) -> requests.Response:
|
|
180
185
|
"""
|
|
181
186
|
Make authenticated request to Microsoft Graph API.
|
|
182
|
-
|
|
187
|
+
|
|
183
188
|
Args:
|
|
184
189
|
method: HTTP method (GET, POST, PUT, etc.)
|
|
185
190
|
endpoint: API endpoint (without base URL)
|
|
186
191
|
**kwargs: Additional arguments for requests
|
|
187
|
-
|
|
192
|
+
|
|
188
193
|
Returns:
|
|
189
194
|
Response object
|
|
190
|
-
|
|
195
|
+
|
|
191
196
|
Raises:
|
|
192
197
|
Exception: If authentication fails or request fails
|
|
193
198
|
"""
|
|
194
199
|
token = get_access_token()
|
|
195
200
|
if not token:
|
|
196
201
|
raise Exception("Failed to get valid access token")
|
|
197
|
-
|
|
198
|
-
headers = kwargs.get(
|
|
199
|
-
headers[
|
|
200
|
-
kwargs[
|
|
201
|
-
|
|
202
|
+
|
|
203
|
+
headers = kwargs.get("headers", {})
|
|
204
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
205
|
+
kwargs["headers"] = headers
|
|
206
|
+
|
|
202
207
|
url = f"{GRAPH_API_BASE}/{endpoint.lstrip('/')}"
|
|
203
208
|
response = requests.request(method, url, **kwargs)
|
|
204
|
-
|
|
209
|
+
|
|
205
210
|
return response
|
|
206
211
|
|
|
207
212
|
|
|
208
213
|
def push_to_onedrive(local_path: str, remote_path: str) -> bool:
|
|
209
214
|
"""
|
|
210
215
|
Push a file from local system to OneDrive.
|
|
211
|
-
|
|
216
|
+
|
|
212
217
|
Args:
|
|
213
218
|
local_path: Path to the local file
|
|
214
219
|
remote_path: Path where the file should be stored in OneDrive
|
|
215
220
|
(e.g., "/Documents/myfile.txt")
|
|
216
|
-
|
|
221
|
+
|
|
217
222
|
Returns:
|
|
218
223
|
True if successful, False otherwise
|
|
219
224
|
"""
|
|
220
225
|
local_file = Path(local_path)
|
|
221
|
-
|
|
226
|
+
|
|
222
227
|
if not local_file.exists():
|
|
223
228
|
print(f"Local file does not exist: {local_path}")
|
|
224
229
|
return False
|
|
225
|
-
|
|
230
|
+
|
|
226
231
|
if not local_file.is_file():
|
|
227
232
|
print(f"Path is not a file: {local_path}")
|
|
228
233
|
return False
|
|
229
|
-
|
|
234
|
+
|
|
230
235
|
# Ensure remote path starts with /
|
|
231
|
-
if not remote_path.startswith(
|
|
232
|
-
remote_path =
|
|
233
|
-
|
|
236
|
+
if not remote_path.startswith("/"):
|
|
237
|
+
remote_path = "/" + remote_path
|
|
238
|
+
|
|
234
239
|
# Create parent directories if they don't exist
|
|
235
240
|
remote_dir = os.path.dirname(remote_path)
|
|
236
|
-
if remote_dir and remote_dir !=
|
|
241
|
+
if remote_dir and remote_dir != "/":
|
|
237
242
|
create_remote_directory(remote_dir)
|
|
238
|
-
|
|
243
|
+
|
|
239
244
|
try:
|
|
240
245
|
file_size = local_file.stat().st_size
|
|
241
|
-
|
|
246
|
+
|
|
242
247
|
# For small files (< 4MB), use simple upload
|
|
243
248
|
if file_size < 4 * 1024 * 1024:
|
|
244
249
|
return simple_upload(local_file, remote_path)
|
|
245
250
|
else:
|
|
246
251
|
return resumable_upload(local_file, remote_path)
|
|
247
|
-
|
|
252
|
+
|
|
248
253
|
except Exception as e:
|
|
249
254
|
print(f"Error uploading file: {e}")
|
|
250
255
|
return False
|
|
@@ -253,23 +258,23 @@ def push_to_onedrive(local_path: str, remote_path: str) -> bool:
|
|
|
253
258
|
def simple_upload(local_file: Path, remote_path: str) -> bool:
|
|
254
259
|
"""Upload small files using simple upload."""
|
|
255
260
|
try:
|
|
256
|
-
with open(local_file,
|
|
261
|
+
with open(local_file, "rb") as f:
|
|
257
262
|
file_content = f.read()
|
|
258
|
-
|
|
263
|
+
|
|
259
264
|
# URL encode the remote path and use specific drive
|
|
260
|
-
encoded_path = quote(remote_path, safe=
|
|
265
|
+
encoded_path = quote(remote_path, safe="/")
|
|
261
266
|
drive_id = get_drive_id()
|
|
262
267
|
endpoint = f"drives/{drive_id}/root:{encoded_path}:/content"
|
|
263
|
-
|
|
264
|
-
response = make_graph_request(
|
|
265
|
-
|
|
268
|
+
|
|
269
|
+
response = make_graph_request("PUT", endpoint, data=file_content)
|
|
270
|
+
|
|
266
271
|
if response.status_code in [200, 201]:
|
|
267
272
|
print(f"Successfully uploaded: {local_file} -> {remote_path}")
|
|
268
273
|
return True
|
|
269
274
|
else:
|
|
270
275
|
print(f"Upload failed: {response.status_code} - {response.text}")
|
|
271
276
|
return False
|
|
272
|
-
|
|
277
|
+
|
|
273
278
|
except Exception as e:
|
|
274
279
|
print(f"Simple upload error: {e}")
|
|
275
280
|
return False
|
|
@@ -279,44 +284,36 @@ def resumable_upload(local_file: Path, remote_path: str) -> bool:
|
|
|
279
284
|
"""Upload large files using resumable upload."""
|
|
280
285
|
try:
|
|
281
286
|
# Create upload session using specific drive
|
|
282
|
-
encoded_path = quote(remote_path, safe=
|
|
287
|
+
encoded_path = quote(remote_path, safe="/")
|
|
283
288
|
drive_id = get_drive_id()
|
|
284
289
|
endpoint = f"drives/{drive_id}/root:{encoded_path}:/createUploadSession"
|
|
285
|
-
|
|
286
|
-
item_data = {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
response = make_graph_request('POST', endpoint, json=item_data)
|
|
294
|
-
|
|
290
|
+
|
|
291
|
+
item_data = {"item": {"@microsoft.graph.conflictBehavior": "replace", "name": local_file.name}}
|
|
292
|
+
|
|
293
|
+
response = make_graph_request("POST", endpoint, json=item_data)
|
|
294
|
+
|
|
295
295
|
if response.status_code != 200:
|
|
296
296
|
print(f"Failed to create upload session: {response.status_code} - {response.text}")
|
|
297
297
|
return False
|
|
298
|
-
|
|
299
|
-
upload_url = response.json()[
|
|
298
|
+
|
|
299
|
+
upload_url = response.json()["uploadUrl"]
|
|
300
300
|
file_size = local_file.stat().st_size
|
|
301
301
|
chunk_size = 320 * 1024 # 320KB chunks
|
|
302
|
-
|
|
303
|
-
with open(local_file,
|
|
302
|
+
|
|
303
|
+
with open(local_file, "rb") as f:
|
|
304
304
|
bytes_uploaded = 0
|
|
305
|
-
|
|
305
|
+
|
|
306
306
|
while bytes_uploaded < file_size:
|
|
307
307
|
chunk_data = f.read(chunk_size)
|
|
308
308
|
if not chunk_data:
|
|
309
309
|
break
|
|
310
|
-
|
|
310
|
+
|
|
311
311
|
chunk_end = min(bytes_uploaded + len(chunk_data) - 1, file_size - 1)
|
|
312
|
-
|
|
313
|
-
headers = {
|
|
314
|
-
|
|
315
|
-
'Content-Length': str(len(chunk_data))
|
|
316
|
-
}
|
|
317
|
-
|
|
312
|
+
|
|
313
|
+
headers = {"Content-Range": f"bytes {bytes_uploaded}-{chunk_end}/{file_size}", "Content-Length": str(len(chunk_data))}
|
|
314
|
+
|
|
318
315
|
chunk_response = requests.put(upload_url, data=chunk_data, headers=headers)
|
|
319
|
-
|
|
316
|
+
|
|
320
317
|
if chunk_response.status_code in [202, 200, 201]:
|
|
321
318
|
bytes_uploaded += len(chunk_data)
|
|
322
319
|
progress = (bytes_uploaded / file_size) * 100
|
|
@@ -324,10 +321,10 @@ def resumable_upload(local_file: Path, remote_path: str) -> bool:
|
|
|
324
321
|
else:
|
|
325
322
|
print(f"Chunk upload failed: {chunk_response.status_code} - {chunk_response.text}")
|
|
326
323
|
return False
|
|
327
|
-
|
|
324
|
+
|
|
328
325
|
print(f"Successfully uploaded: {local_file} -> {remote_path}")
|
|
329
326
|
return True
|
|
330
|
-
|
|
327
|
+
|
|
331
328
|
except Exception as e:
|
|
332
329
|
print(f"Resumable upload error: {e}")
|
|
333
330
|
return False
|
|
@@ -336,70 +333,70 @@ def resumable_upload(local_file: Path, remote_path: str) -> bool:
|
|
|
336
333
|
def pull_from_onedrive(remote_path: str, local_path: str) -> bool:
|
|
337
334
|
"""
|
|
338
335
|
Pull a file from OneDrive to local system.
|
|
339
|
-
|
|
336
|
+
|
|
340
337
|
Args:
|
|
341
338
|
remote_path: Path to the file in OneDrive (e.g., "/Documents/myfile.txt")
|
|
342
339
|
local_path: Path where the file should be saved locally
|
|
343
|
-
|
|
340
|
+
|
|
344
341
|
Returns:
|
|
345
342
|
True if successful, False otherwise
|
|
346
343
|
"""
|
|
347
344
|
# Ensure remote path starts with /
|
|
348
|
-
if not remote_path.startswith(
|
|
349
|
-
remote_path =
|
|
350
|
-
|
|
345
|
+
if not remote_path.startswith("/"):
|
|
346
|
+
remote_path = "/" + remote_path
|
|
347
|
+
|
|
351
348
|
try:
|
|
352
349
|
# Get file metadata and download URL using specific drive
|
|
353
|
-
encoded_path = quote(remote_path, safe=
|
|
350
|
+
encoded_path = quote(remote_path, safe="/")
|
|
354
351
|
drive_id = get_drive_id()
|
|
355
352
|
endpoint = f"drives/{drive_id}/root:{encoded_path}"
|
|
356
|
-
|
|
357
|
-
response = make_graph_request(
|
|
358
|
-
|
|
353
|
+
|
|
354
|
+
response = make_graph_request("GET", endpoint)
|
|
355
|
+
|
|
359
356
|
if response.status_code == 404:
|
|
360
357
|
print(f"File not found in OneDrive: {remote_path}")
|
|
361
358
|
return False
|
|
362
359
|
elif response.status_code != 200:
|
|
363
360
|
print(f"Failed to get file info: {response.status_code} - {response.text}")
|
|
364
361
|
return False
|
|
365
|
-
|
|
362
|
+
|
|
366
363
|
file_info = response.json()
|
|
367
|
-
|
|
364
|
+
|
|
368
365
|
# Check if it's a file (not a folder)
|
|
369
|
-
if
|
|
366
|
+
if "folder" in file_info:
|
|
370
367
|
print(f"Path is a folder, not a file: {remote_path}")
|
|
371
368
|
return False
|
|
372
|
-
|
|
369
|
+
|
|
373
370
|
# Get download URL
|
|
374
|
-
download_url = file_info.get(
|
|
371
|
+
download_url = file_info.get("@microsoft.graph.downloadUrl")
|
|
375
372
|
if not download_url:
|
|
376
373
|
print("No download URL available")
|
|
377
374
|
return False
|
|
378
|
-
|
|
375
|
+
|
|
379
376
|
# Create local directory if it doesn't exist
|
|
380
377
|
local_file = Path(local_path)
|
|
381
378
|
local_file.parent.mkdir(parents=True, exist_ok=True)
|
|
382
|
-
|
|
379
|
+
|
|
383
380
|
# Download the file
|
|
384
381
|
download_response = requests.get(download_url, stream=True)
|
|
385
382
|
download_response.raise_for_status()
|
|
386
|
-
|
|
387
|
-
file_size = int(file_info.get(
|
|
383
|
+
|
|
384
|
+
file_size = int(file_info.get("size", 0))
|
|
388
385
|
bytes_downloaded = 0
|
|
389
|
-
|
|
390
|
-
with open(local_file,
|
|
386
|
+
|
|
387
|
+
with open(local_file, "wb") as f:
|
|
391
388
|
for chunk in download_response.iter_content(chunk_size=8192):
|
|
392
389
|
if chunk:
|
|
393
390
|
f.write(chunk)
|
|
394
391
|
bytes_downloaded += len(chunk)
|
|
395
|
-
|
|
392
|
+
|
|
396
393
|
if file_size > 0:
|
|
397
394
|
progress = (bytes_downloaded / file_size) * 100
|
|
398
395
|
print(f"Download progress: {progress:.1f}%")
|
|
399
|
-
|
|
396
|
+
|
|
400
397
|
print(f"Successfully downloaded: {remote_path} -> {local_path}")
|
|
401
398
|
return True
|
|
402
|
-
|
|
399
|
+
|
|
403
400
|
except Exception as e:
|
|
404
401
|
print(f"Error downloading file: {e}")
|
|
405
402
|
return False
|
|
@@ -408,64 +405,60 @@ def pull_from_onedrive(remote_path: str, local_path: str) -> bool:
|
|
|
408
405
|
def create_remote_directory(remote_path: str) -> bool:
|
|
409
406
|
"""
|
|
410
407
|
Create a directory in OneDrive if it doesn't exist.
|
|
411
|
-
|
|
408
|
+
|
|
412
409
|
Args:
|
|
413
410
|
remote_path: Path to the directory in OneDrive
|
|
414
|
-
|
|
411
|
+
|
|
415
412
|
Returns:
|
|
416
413
|
True if successful or already exists, False otherwise
|
|
417
414
|
"""
|
|
418
|
-
if not remote_path or remote_path ==
|
|
415
|
+
if not remote_path or remote_path == "/":
|
|
419
416
|
return True
|
|
420
|
-
|
|
417
|
+
|
|
421
418
|
# Ensure remote path starts with /
|
|
422
|
-
if not remote_path.startswith(
|
|
423
|
-
remote_path =
|
|
424
|
-
|
|
419
|
+
if not remote_path.startswith("/"):
|
|
420
|
+
remote_path = "/" + remote_path
|
|
421
|
+
|
|
425
422
|
try:
|
|
426
423
|
# Check if directory already exists using specific drive
|
|
427
|
-
encoded_path = quote(remote_path, safe=
|
|
424
|
+
encoded_path = quote(remote_path, safe="/")
|
|
428
425
|
drive_id = get_drive_id()
|
|
429
426
|
endpoint = f"drives/{drive_id}/root:{encoded_path}"
|
|
430
|
-
|
|
431
|
-
response = make_graph_request(
|
|
432
|
-
|
|
427
|
+
|
|
428
|
+
response = make_graph_request("GET", endpoint)
|
|
429
|
+
|
|
433
430
|
if response.status_code == 200:
|
|
434
431
|
# Directory already exists
|
|
435
432
|
return True
|
|
436
433
|
elif response.status_code != 404:
|
|
437
434
|
print(f"Error checking directory: {response.status_code} - {response.text}")
|
|
438
435
|
return False
|
|
439
|
-
|
|
436
|
+
|
|
440
437
|
# Create parent directory first
|
|
441
438
|
parent_dir = os.path.dirname(remote_path)
|
|
442
|
-
if parent_dir and parent_dir !=
|
|
439
|
+
if parent_dir and parent_dir != "/":
|
|
443
440
|
if not create_remote_directory(parent_dir):
|
|
444
441
|
return False
|
|
445
|
-
|
|
442
|
+
|
|
446
443
|
# Create the directory
|
|
447
444
|
dir_name = os.path.basename(remote_path)
|
|
448
|
-
parent_encoded = quote(parent_dir if parent_dir else
|
|
449
|
-
|
|
450
|
-
if parent_dir and parent_dir !=
|
|
445
|
+
parent_encoded = quote(parent_dir if parent_dir else "/", safe="/")
|
|
446
|
+
|
|
447
|
+
if parent_dir and parent_dir != "/":
|
|
451
448
|
endpoint = f"drives/{drive_id}/root:{parent_encoded}:/children"
|
|
452
449
|
else:
|
|
453
450
|
endpoint = f"drives/{drive_id}/root/children"
|
|
454
|
-
|
|
455
|
-
folder_data = {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
response = make_graph_request('POST', endpoint, json=folder_data)
|
|
462
|
-
|
|
451
|
+
|
|
452
|
+
folder_data = {"name": dir_name, "folder": {}, "@microsoft.graph.conflictBehavior": "replace"}
|
|
453
|
+
|
|
454
|
+
response = make_graph_request("POST", endpoint, json=folder_data)
|
|
455
|
+
|
|
463
456
|
if response.status_code in [200, 201]:
|
|
464
457
|
return True
|
|
465
458
|
else:
|
|
466
459
|
print(f"Failed to create directory: {response.status_code} - {response.text}")
|
|
467
460
|
return False
|
|
468
|
-
|
|
461
|
+
|
|
469
462
|
except Exception as e:
|
|
470
463
|
print(f"Error creating directory: {e}")
|
|
471
464
|
return False
|
|
@@ -474,7 +467,7 @@ def create_remote_directory(remote_path: str) -> bool:
|
|
|
474
467
|
def refresh_access_token() -> Optional[dict[str, Any]]:
|
|
475
468
|
"""
|
|
476
469
|
Refresh the access token using the refresh token.
|
|
477
|
-
|
|
470
|
+
|
|
478
471
|
Returns:
|
|
479
472
|
New token dictionary with access_token, refresh_token, and expiry, or None if failed
|
|
480
473
|
"""
|
|
@@ -483,63 +476,56 @@ def refresh_access_token() -> Optional[dict[str, Any]]:
|
|
|
483
476
|
if not refresh_token:
|
|
484
477
|
print("ERROR: No refresh token available!")
|
|
485
478
|
return None
|
|
486
|
-
|
|
479
|
+
|
|
487
480
|
print("🔄 Refreshing access token...")
|
|
488
|
-
|
|
481
|
+
|
|
489
482
|
# Prepare the token refresh request
|
|
490
|
-
data = {
|
|
491
|
-
|
|
492
|
-
'grant_type': 'refresh_token',
|
|
493
|
-
'refresh_token': refresh_token,
|
|
494
|
-
'scope': 'https://graph.microsoft.com/Files.ReadWrite.All offline_access'
|
|
495
|
-
}
|
|
496
|
-
|
|
483
|
+
data = {"client_id": CLIENT_ID, "grant_type": "refresh_token", "refresh_token": refresh_token, "scope": "https://graph.microsoft.com/Files.ReadWrite.All offline_access"}
|
|
484
|
+
|
|
497
485
|
# Add client secret if available (for confidential clients)
|
|
498
486
|
if CLIENT_SECRET and CLIENT_SECRET != "your_client_secret_here":
|
|
499
|
-
data[
|
|
500
|
-
|
|
501
|
-
headers = {
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
487
|
+
data["client_secret"] = CLIENT_SECRET
|
|
488
|
+
|
|
489
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
490
|
+
|
|
505
491
|
try:
|
|
506
492
|
response = requests.post(OAUTH_TOKEN_ENDPOINT, data=data, headers=headers)
|
|
507
|
-
|
|
493
|
+
|
|
508
494
|
if response.status_code == 200:
|
|
509
495
|
token_data = response.json()
|
|
510
|
-
|
|
496
|
+
|
|
511
497
|
# Calculate expiry time (tokens typically last 1 hour)
|
|
512
|
-
expires_in = token_data.get(
|
|
498
|
+
expires_in = token_data.get("expires_in", 3600) # Default to 1 hour
|
|
513
499
|
expiry_time = datetime.now() + timedelta(seconds=expires_in)
|
|
514
|
-
|
|
500
|
+
|
|
515
501
|
# Update the cached token configuration
|
|
516
502
|
new_token = {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
503
|
+
"access_token": token_data["access_token"],
|
|
504
|
+
"token_type": token_data.get("token_type", "Bearer"),
|
|
505
|
+
"refresh_token": token_data.get("refresh_token", refresh_token), # Use new or keep old
|
|
506
|
+
"expiry": expiry_time.isoformat(),
|
|
521
507
|
}
|
|
522
|
-
|
|
508
|
+
|
|
523
509
|
# Update the cached config
|
|
524
510
|
global _cached_config
|
|
525
511
|
if _cached_config is not None:
|
|
526
512
|
_cached_config["token"] = new_token
|
|
527
513
|
else:
|
|
528
514
|
clear_config_cache() # Force reload on next access
|
|
529
|
-
|
|
515
|
+
|
|
530
516
|
print("✅ Access token refreshed successfully!")
|
|
531
517
|
print(f"🕒 New token expires at: {expiry_time}")
|
|
532
|
-
|
|
518
|
+
|
|
533
519
|
# Optionally save the new token to a file for persistence
|
|
534
520
|
save_token_to_file(new_token)
|
|
535
|
-
|
|
521
|
+
|
|
536
522
|
return new_token
|
|
537
|
-
|
|
523
|
+
|
|
538
524
|
else:
|
|
539
525
|
print(f"❌ Token refresh failed: {response.status_code}")
|
|
540
526
|
print(f"Response: {response.text}")
|
|
541
527
|
return None
|
|
542
|
-
|
|
528
|
+
|
|
543
529
|
except Exception as e:
|
|
544
530
|
print(f"❌ Error refreshing token: {e}")
|
|
545
531
|
return None
|
|
@@ -548,31 +534,31 @@ def refresh_access_token() -> Optional[dict[str, Any]]:
|
|
|
548
534
|
def save_token_to_file(token_data: dict[str, Any], file_path: Optional[str] = None) -> bool:
|
|
549
535
|
"""
|
|
550
536
|
Save token data to a file for persistence.
|
|
551
|
-
|
|
537
|
+
|
|
552
538
|
Args:
|
|
553
539
|
token_data: Token dictionary to save
|
|
554
540
|
file_path: Optional path to save the token file
|
|
555
|
-
|
|
541
|
+
|
|
556
542
|
Returns:
|
|
557
543
|
True if successful, False otherwise
|
|
558
544
|
"""
|
|
559
545
|
if not file_path:
|
|
560
546
|
# Default to a hidden file in user's home directory
|
|
561
547
|
file_path = os.path.expanduser("~/.onedrive_token.json")
|
|
562
|
-
|
|
548
|
+
|
|
563
549
|
try:
|
|
564
550
|
# Create directory if it doesn't exist
|
|
565
551
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
566
|
-
|
|
567
|
-
with open(file_path,
|
|
552
|
+
|
|
553
|
+
with open(file_path, "w") as f:
|
|
568
554
|
json.dump(token_data, f, indent=2)
|
|
569
|
-
|
|
555
|
+
|
|
570
556
|
# Set restrictive permissions (readable only by owner)
|
|
571
557
|
os.chmod(file_path, 0o600)
|
|
572
|
-
|
|
558
|
+
|
|
573
559
|
print(f"💾 Token saved to: {file_path}")
|
|
574
560
|
return True
|
|
575
|
-
|
|
561
|
+
|
|
576
562
|
except Exception as e:
|
|
577
563
|
print(f"❌ Error saving token: {e}")
|
|
578
564
|
return False
|
|
@@ -581,34 +567,34 @@ def save_token_to_file(token_data: dict[str, Any], file_path: Optional[str] = No
|
|
|
581
567
|
def load_token_from_file(file_path: Optional[str] = None) -> Optional[dict[str, Any]]:
|
|
582
568
|
"""
|
|
583
569
|
Load token data from a file.
|
|
584
|
-
|
|
570
|
+
|
|
585
571
|
Args:
|
|
586
572
|
file_path: Optional path to load the token file from
|
|
587
|
-
|
|
573
|
+
|
|
588
574
|
Returns:
|
|
589
575
|
Token dictionary or None if failed
|
|
590
576
|
"""
|
|
591
577
|
if not file_path:
|
|
592
578
|
file_path = os.path.expanduser("~/.onedrive_token.json")
|
|
593
|
-
|
|
579
|
+
|
|
594
580
|
try:
|
|
595
581
|
if os.path.exists(file_path):
|
|
596
|
-
with open(file_path,
|
|
582
|
+
with open(file_path, "r") as f:
|
|
597
583
|
token_data = json.load(f)
|
|
598
|
-
|
|
584
|
+
|
|
599
585
|
# Update the cached config token
|
|
600
586
|
global _cached_config
|
|
601
587
|
if _cached_config is not None:
|
|
602
588
|
_cached_config["token"] = token_data
|
|
603
589
|
else:
|
|
604
590
|
clear_config_cache() # Force reload on next access
|
|
605
|
-
|
|
591
|
+
|
|
606
592
|
print(f"📂 Token loaded from: {file_path}")
|
|
607
593
|
return token_data
|
|
608
594
|
else:
|
|
609
595
|
print(f"ℹ️ No saved token file found at: {file_path}")
|
|
610
596
|
return None
|
|
611
|
-
|
|
597
|
+
|
|
612
598
|
except Exception as e:
|
|
613
599
|
print(f"❌ Error loading token: {e}")
|
|
614
600
|
return None
|
|
@@ -618,21 +604,14 @@ def get_authorization_url() -> str:
|
|
|
618
604
|
"""
|
|
619
605
|
Generate the authorization URL for initial OAuth setup.
|
|
620
606
|
This is needed only for the first-time setup to get the initial tokens.
|
|
621
|
-
|
|
607
|
+
|
|
622
608
|
Returns:
|
|
623
609
|
Authorization URL string
|
|
624
610
|
"""
|
|
625
611
|
from urllib.parse import urlencode
|
|
626
|
-
|
|
627
|
-
params = {
|
|
628
|
-
|
|
629
|
-
'response_type': 'code',
|
|
630
|
-
'redirect_uri': REDIRECT_URI,
|
|
631
|
-
'response_mode': 'query',
|
|
632
|
-
'scope': 'https://graph.microsoft.com/Files.ReadWrite.All offline_access',
|
|
633
|
-
'state': 'onedrive_auth'
|
|
634
|
-
}
|
|
635
|
-
|
|
612
|
+
|
|
613
|
+
params = {"client_id": CLIENT_ID, "response_type": "code", "redirect_uri": REDIRECT_URI, "response_mode": "query", "scope": "https://graph.microsoft.com/Files.ReadWrite.All offline_access", "state": "onedrive_auth"}
|
|
614
|
+
|
|
636
615
|
auth_url = f"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?{urlencode(params)}"
|
|
637
616
|
return auth_url
|
|
638
617
|
|
|
@@ -641,46 +620,33 @@ def exchange_authorization_code(authorization_code: str) -> Optional[dict[str, A
|
|
|
641
620
|
"""
|
|
642
621
|
Exchange authorization code for initial tokens.
|
|
643
622
|
This is used during the first-time OAuth setup.
|
|
644
|
-
|
|
623
|
+
|
|
645
624
|
Args:
|
|
646
625
|
authorization_code: The authorization code received from the callback
|
|
647
|
-
|
|
626
|
+
|
|
648
627
|
Returns:
|
|
649
628
|
Token dictionary or None if failed
|
|
650
629
|
"""
|
|
651
|
-
data = {
|
|
652
|
-
|
|
653
|
-
'grant_type': 'authorization_code',
|
|
654
|
-
'code': authorization_code,
|
|
655
|
-
'redirect_uri': REDIRECT_URI,
|
|
656
|
-
'scope': 'https://graph.microsoft.com/Files.ReadWrite.All offline_access'
|
|
657
|
-
}
|
|
658
|
-
|
|
630
|
+
data = {"client_id": CLIENT_ID, "grant_type": "authorization_code", "code": authorization_code, "redirect_uri": REDIRECT_URI, "scope": "https://graph.microsoft.com/Files.ReadWrite.All offline_access"}
|
|
631
|
+
|
|
659
632
|
# Add client secret if available
|
|
660
633
|
if CLIENT_SECRET and CLIENT_SECRET != "your_client_secret_here":
|
|
661
|
-
data[
|
|
662
|
-
|
|
663
|
-
headers = {
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
|
|
634
|
+
data["client_secret"] = CLIENT_SECRET
|
|
635
|
+
|
|
636
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
637
|
+
|
|
667
638
|
try:
|
|
668
639
|
response = requests.post(OAUTH_TOKEN_ENDPOINT, data=data, headers=headers)
|
|
669
|
-
|
|
640
|
+
|
|
670
641
|
if response.status_code == 200:
|
|
671
642
|
token_data = response.json()
|
|
672
|
-
|
|
643
|
+
|
|
673
644
|
# Calculate expiry time
|
|
674
|
-
expires_in = token_data.get(
|
|
645
|
+
expires_in = token_data.get("expires_in", 3600)
|
|
675
646
|
expiry_time = datetime.now() + timedelta(seconds=expires_in)
|
|
676
|
-
|
|
677
|
-
new_token = {
|
|
678
|
-
|
|
679
|
-
'token_type': token_data.get('token_type', 'Bearer'),
|
|
680
|
-
'refresh_token': token_data['refresh_token'],
|
|
681
|
-
'expiry': expiry_time.isoformat()
|
|
682
|
-
}
|
|
683
|
-
|
|
647
|
+
|
|
648
|
+
new_token = {"access_token": token_data["access_token"], "token_type": token_data.get("token_type", "Bearer"), "refresh_token": token_data["refresh_token"], "expiry": expiry_time.isoformat()}
|
|
649
|
+
|
|
684
650
|
# Update cached config and save
|
|
685
651
|
global _cached_config
|
|
686
652
|
if _cached_config is not None:
|
|
@@ -688,15 +654,15 @@ def exchange_authorization_code(authorization_code: str) -> Optional[dict[str, A
|
|
|
688
654
|
else:
|
|
689
655
|
clear_config_cache() # Force reload on next access
|
|
690
656
|
save_token_to_file(new_token)
|
|
691
|
-
|
|
657
|
+
|
|
692
658
|
print("✅ Initial tokens obtained successfully!")
|
|
693
659
|
return new_token
|
|
694
|
-
|
|
660
|
+
|
|
695
661
|
else:
|
|
696
662
|
print(f"❌ Token exchange failed: {response.status_code}")
|
|
697
663
|
print(f"Response: {response.text}")
|
|
698
664
|
return None
|
|
699
|
-
|
|
665
|
+
|
|
700
666
|
except Exception as e:
|
|
701
667
|
print(f"❌ Error exchanging authorization code: {e}")
|
|
702
668
|
return None
|
|
@@ -709,7 +675,7 @@ def setup_oauth_authentication():
|
|
|
709
675
|
"""
|
|
710
676
|
print("🔧 Setting up OneDrive OAuth Authentication")
|
|
711
677
|
print("=" * 50)
|
|
712
|
-
|
|
678
|
+
|
|
713
679
|
if CLIENT_ID == "your_client_id_here":
|
|
714
680
|
print("❌ You need to set up Azure App Registration first!")
|
|
715
681
|
print("\n📋 Setup Instructions:")
|
|
@@ -725,21 +691,21 @@ def setup_oauth_authentication():
|
|
|
725
691
|
print(" export ONEDRIVE_CLIENT_ID='your_client_id'")
|
|
726
692
|
print(" export ONEDRIVE_REDIRECT_URI='http://localhost:8080/callback'")
|
|
727
693
|
return
|
|
728
|
-
|
|
694
|
+
|
|
729
695
|
print(f"Using Client ID: {CLIENT_ID}")
|
|
730
696
|
print(f"Redirect URI: {REDIRECT_URI}")
|
|
731
|
-
|
|
697
|
+
|
|
732
698
|
# Generate authorization URL
|
|
733
699
|
auth_url = get_authorization_url()
|
|
734
700
|
print("\n🌐 Please visit this URL to authorize the application:")
|
|
735
701
|
print(f"{auth_url}")
|
|
736
|
-
|
|
702
|
+
|
|
737
703
|
print("\n📋 After authorization, you'll be redirected to:")
|
|
738
704
|
print(f"{REDIRECT_URI}?code=AUTHORIZATION_CODE&state=onedrive_auth")
|
|
739
705
|
print("\n🔑 Copy the 'code' parameter from the URL and paste it below:")
|
|
740
|
-
|
|
706
|
+
|
|
741
707
|
auth_code = input("Authorization Code: ").strip()
|
|
742
|
-
|
|
708
|
+
|
|
743
709
|
if auth_code:
|
|
744
710
|
token_data = exchange_authorization_code(auth_code)
|
|
745
711
|
if token_data:
|
|
@@ -755,18 +721,18 @@ def setup_oauth_authentication():
|
|
|
755
721
|
if __name__ == "__main__":
|
|
756
722
|
# Try to load existing token from file
|
|
757
723
|
load_token_from_file()
|
|
758
|
-
|
|
724
|
+
|
|
759
725
|
print("OneDrive transaction functions loaded.")
|
|
760
726
|
try:
|
|
761
727
|
config = get_config()
|
|
762
728
|
print(f"Drive ID: {get_drive_id()}")
|
|
763
729
|
print(f"Drive Type: {get_drive_type()}")
|
|
764
|
-
|
|
730
|
+
|
|
765
731
|
if is_token_valid():
|
|
766
732
|
print("✅ Token is valid and ready to use")
|
|
767
733
|
else:
|
|
768
734
|
print("⚠️ Token has expired or is invalid")
|
|
769
|
-
|
|
735
|
+
|
|
770
736
|
# Try to refresh automatically
|
|
771
737
|
if refresh_access_token():
|
|
772
738
|
print("✅ Token refreshed successfully")
|
|
@@ -778,7 +744,7 @@ if __name__ == "__main__":
|
|
|
778
744
|
except Exception as e:
|
|
779
745
|
print(f"❌ Error loading rclone config: {e}")
|
|
780
746
|
print("Please ensure rclone is configured with an 'odp' section")
|
|
781
|
-
|
|
747
|
+
|
|
782
748
|
print("\n📚 Available Functions:")
|
|
783
749
|
print("• push_to_onedrive(local_path, remote_path)")
|
|
784
750
|
print("• pull_from_onedrive(remote_path, local_path)")
|
|
@@ -786,11 +752,10 @@ if __name__ == "__main__":
|
|
|
786
752
|
print("• setup_oauth_authentication() - First-time OAuth setup")
|
|
787
753
|
print("• save_token_to_file(token_data) - Save tokens for persistence")
|
|
788
754
|
print("• load_token_from_file() - Load saved tokens")
|
|
789
|
-
|
|
755
|
+
|
|
790
756
|
print("\n💡 Example usage:")
|
|
791
757
|
print("push_to_onedrive('/home/user/document.pdf', '/Documents/document.pdf')")
|
|
792
758
|
print("pull_from_onedrive('/Documents/document.pdf', '/home/user/downloaded.pdf')")
|
|
793
|
-
|
|
759
|
+
|
|
794
760
|
# Uncomment to test with a file
|
|
795
761
|
# push_to_onedrive('/home/alex/Downloads/users.xlsx', '/Documents/users.xlsx')
|
|
796
|
-
|