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