dedrive 0.1.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.
- dedrive/__init__.py +84 -0
- dedrive/cli.py +259 -0
- dedrive/config.py +284 -0
- dedrive/dedup.py +156 -0
- dedrive/drive.py +347 -0
- dedrive/profiles.py +78 -0
- dedrive/ui.py +1450 -0
- dedrive-0.1.0.dist-info/METADATA +224 -0
- dedrive-0.1.0.dist-info/RECORD +12 -0
- dedrive-0.1.0.dist-info/WHEEL +4 -0
- dedrive-0.1.0.dist-info/entry_points.txt +2 -0
- dedrive-0.1.0.dist-info/licenses/LICENSE +21 -0
dedrive/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Google Drive Deduplication Tool - package API."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from dedrive.drive import (
|
|
6
|
+
SCOPES,
|
|
7
|
+
setup_logging,
|
|
8
|
+
authenticate,
|
|
9
|
+
create_oauth_flow,
|
|
10
|
+
run_oauth_callback_server,
|
|
11
|
+
save_token,
|
|
12
|
+
load_existing_token,
|
|
13
|
+
get_user_info,
|
|
14
|
+
fetch_with_retry,
|
|
15
|
+
fetch_all_files,
|
|
16
|
+
build_lookups,
|
|
17
|
+
get_path,
|
|
18
|
+
)
|
|
19
|
+
from dedrive.dedup import (
|
|
20
|
+
filter_by_path,
|
|
21
|
+
filter_excluded_paths,
|
|
22
|
+
find_duplicates,
|
|
23
|
+
write_csv,
|
|
24
|
+
calculate_savings,
|
|
25
|
+
format_size,
|
|
26
|
+
)
|
|
27
|
+
from dedrive.config import (
|
|
28
|
+
get_exclude_paths,
|
|
29
|
+
get_credentials_path,
|
|
30
|
+
get_token_path,
|
|
31
|
+
get_output_dir,
|
|
32
|
+
get_dupes_folder,
|
|
33
|
+
get_batch_size,
|
|
34
|
+
get_max_preview_size,
|
|
35
|
+
set_active_profile,
|
|
36
|
+
set_active_profile_from_email,
|
|
37
|
+
create_default_config,
|
|
38
|
+
print_config,
|
|
39
|
+
)
|
|
40
|
+
from dedrive.profiles import (
|
|
41
|
+
init_profile,
|
|
42
|
+
list_profiles,
|
|
43
|
+
delete_profile_token,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"__version__",
|
|
48
|
+
# drive
|
|
49
|
+
"SCOPES",
|
|
50
|
+
"setup_logging",
|
|
51
|
+
"authenticate",
|
|
52
|
+
"create_oauth_flow",
|
|
53
|
+
"run_oauth_callback_server",
|
|
54
|
+
"save_token",
|
|
55
|
+
"load_existing_token",
|
|
56
|
+
"get_user_info",
|
|
57
|
+
"fetch_with_retry",
|
|
58
|
+
"fetch_all_files",
|
|
59
|
+
"build_lookups",
|
|
60
|
+
"get_path",
|
|
61
|
+
# dedup
|
|
62
|
+
"filter_by_path",
|
|
63
|
+
"filter_excluded_paths",
|
|
64
|
+
"find_duplicates",
|
|
65
|
+
"write_csv",
|
|
66
|
+
"calculate_savings",
|
|
67
|
+
"format_size",
|
|
68
|
+
# config
|
|
69
|
+
"get_exclude_paths",
|
|
70
|
+
"get_credentials_path",
|
|
71
|
+
"get_token_path",
|
|
72
|
+
"get_output_dir",
|
|
73
|
+
"get_dupes_folder",
|
|
74
|
+
"get_batch_size",
|
|
75
|
+
"get_max_preview_size",
|
|
76
|
+
"set_active_profile",
|
|
77
|
+
"set_active_profile_from_email",
|
|
78
|
+
"create_default_config",
|
|
79
|
+
"print_config",
|
|
80
|
+
# profiles
|
|
81
|
+
"init_profile",
|
|
82
|
+
"list_profiles",
|
|
83
|
+
"delete_profile_token",
|
|
84
|
+
]
|
dedrive/cli.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""CLI entry point for dedrive."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import webbrowser
|
|
7
|
+
|
|
8
|
+
from dedrive.config import (
|
|
9
|
+
get_credentials_path,
|
|
10
|
+
get_token_path,
|
|
11
|
+
set_active_profile,
|
|
12
|
+
set_active_profile_from_email,
|
|
13
|
+
)
|
|
14
|
+
from dedrive.drive import (
|
|
15
|
+
create_oauth_flow,
|
|
16
|
+
run_oauth_callback_server,
|
|
17
|
+
save_token,
|
|
18
|
+
load_existing_token,
|
|
19
|
+
get_user_info,
|
|
20
|
+
setup_logging,
|
|
21
|
+
authenticate,
|
|
22
|
+
)
|
|
23
|
+
from dedrive.profiles import (
|
|
24
|
+
list_profiles,
|
|
25
|
+
get_profile_token_path,
|
|
26
|
+
delete_profile_token,
|
|
27
|
+
PROFILES_DIR,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cmd_login(args):
|
|
32
|
+
"""Handle the login subcommand."""
|
|
33
|
+
credentials_path = get_credentials_path()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
auth_url, flow, port = create_oauth_flow(credentials_path)
|
|
37
|
+
except FileNotFoundError as e:
|
|
38
|
+
print(f"Error: {e}")
|
|
39
|
+
print()
|
|
40
|
+
print("To fix this:")
|
|
41
|
+
print(" 1. Go to https://console.cloud.google.com/apis/credentials")
|
|
42
|
+
print(" 2. Create a project (if you haven't already)")
|
|
43
|
+
print(" 3. Enable the Google Drive API")
|
|
44
|
+
print(" 4. Create OAuth 2.0 Client ID (choose 'Desktop app')")
|
|
45
|
+
print(" 5. Download the JSON file")
|
|
46
|
+
print(f" 6. Save it as: {credentials_path}")
|
|
47
|
+
print(f" Or save it to: {PROFILES_DIR / 'credentials.json'}")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
print(f"Opening browser for Google sign-in...")
|
|
51
|
+
print(f"If the browser doesn't open, visit: {auth_url}")
|
|
52
|
+
webbrowser.open(auth_url)
|
|
53
|
+
|
|
54
|
+
print("Waiting for authentication...")
|
|
55
|
+
try:
|
|
56
|
+
creds = run_oauth_callback_server(flow, port)
|
|
57
|
+
except TimeoutError:
|
|
58
|
+
print("Error: Authentication timed out.")
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"Error: Authentication failed: {e}")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
# Build service to get user info
|
|
65
|
+
from googleapiclient.discovery import build
|
|
66
|
+
service = build("drive", "v3", credentials=creds)
|
|
67
|
+
user_info = get_user_info(service)
|
|
68
|
+
email = user_info["email"]
|
|
69
|
+
name = user_info["name"]
|
|
70
|
+
|
|
71
|
+
# Create/activate profile based on email
|
|
72
|
+
profile_name = set_active_profile_from_email(email)
|
|
73
|
+
|
|
74
|
+
# Save token to profile
|
|
75
|
+
token_path = get_token_path()
|
|
76
|
+
save_token(creds, token_path)
|
|
77
|
+
|
|
78
|
+
display_name = f"{name} ({email})" if name else email
|
|
79
|
+
print(f"Logged in as {display_name}")
|
|
80
|
+
print(f"Profile: {profile_name}")
|
|
81
|
+
print(f"Token saved to: {token_path}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cmd_logout(args):
|
|
85
|
+
"""Handle the logout subcommand."""
|
|
86
|
+
profile = args.profile
|
|
87
|
+
|
|
88
|
+
if not profile:
|
|
89
|
+
# Auto-detect: find profiles with tokens
|
|
90
|
+
logged_in = []
|
|
91
|
+
for name in list_profiles():
|
|
92
|
+
token_path = get_profile_token_path(name)
|
|
93
|
+
if token_path.exists():
|
|
94
|
+
logged_in.append(name)
|
|
95
|
+
|
|
96
|
+
if not logged_in:
|
|
97
|
+
print("No logged-in profiles found.")
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
elif len(logged_in) == 1:
|
|
100
|
+
profile = logged_in[0]
|
|
101
|
+
else:
|
|
102
|
+
print("Multiple logged-in profiles found. Specify one with --profile:")
|
|
103
|
+
for name in logged_in:
|
|
104
|
+
print(f" dedrive logout --profile {name}")
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
deleted = delete_profile_token(profile)
|
|
108
|
+
if deleted:
|
|
109
|
+
print(f"Logged out from profile: {profile}")
|
|
110
|
+
else:
|
|
111
|
+
print(f"Profile '{profile}' was not logged in.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cmd_list_profiles(args):
|
|
115
|
+
"""Handle --list-profiles."""
|
|
116
|
+
profiles = list_profiles()
|
|
117
|
+
if not profiles:
|
|
118
|
+
print("No profiles found. Run 'dedrive login' to create one.")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
print("Profiles:")
|
|
122
|
+
for name in profiles:
|
|
123
|
+
token_path = get_profile_token_path(name)
|
|
124
|
+
status = " (logged in)" if token_path.exists() else ""
|
|
125
|
+
print(f" {name}{status}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cmd_ui(args):
|
|
129
|
+
"""Handle the default command (launch Gradio UI)."""
|
|
130
|
+
if not args.profile:
|
|
131
|
+
profiles = list_profiles()
|
|
132
|
+
if not profiles:
|
|
133
|
+
print("No profiles found. Run 'dedrive login' first.")
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
|
|
136
|
+
logged_in = [
|
|
137
|
+
name for name in profiles
|
|
138
|
+
if get_profile_token_path(name).exists()
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
if len(logged_in) == 1:
|
|
142
|
+
args.profile = logged_in[0]
|
|
143
|
+
print(f"Using profile: {args.profile}")
|
|
144
|
+
else:
|
|
145
|
+
display = logged_in if logged_in else profiles
|
|
146
|
+
print("Select a profile:")
|
|
147
|
+
for i, name in enumerate(display, 1):
|
|
148
|
+
status = " (logged in)" if name in logged_in else ""
|
|
149
|
+
print(f" {i}) {name}{status}")
|
|
150
|
+
print()
|
|
151
|
+
try:
|
|
152
|
+
choice = input("Enter number: ").strip()
|
|
153
|
+
idx = int(choice) - 1
|
|
154
|
+
if 0 <= idx < len(display):
|
|
155
|
+
args.profile = display[idx]
|
|
156
|
+
else:
|
|
157
|
+
print("Invalid selection.")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
except (ValueError, EOFError, KeyboardInterrupt):
|
|
160
|
+
print()
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
|
|
163
|
+
set_active_profile(args.profile)
|
|
164
|
+
|
|
165
|
+
setup_logging(verbose=args.verbose, log_file=args.log_file)
|
|
166
|
+
|
|
167
|
+
if args.validate:
|
|
168
|
+
logger = logging.getLogger(__name__)
|
|
169
|
+
credentials_path = get_credentials_path()
|
|
170
|
+
try:
|
|
171
|
+
from googleapiclient.discovery import build
|
|
172
|
+
creds = authenticate(credentials_path)
|
|
173
|
+
service = build("drive", "v3", credentials=creds)
|
|
174
|
+
user_info = get_user_info(service)
|
|
175
|
+
logger.info(f"Credentials valid. Connected as: {user_info['name']} <{user_info['email']}>")
|
|
176
|
+
sys.exit(0)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Credential validation failed: {e}")
|
|
179
|
+
sys.exit(1)
|
|
180
|
+
|
|
181
|
+
from dedrive.ui import create_ui
|
|
182
|
+
from dedrive.config import get_output_dir
|
|
183
|
+
app = create_ui()
|
|
184
|
+
app.launch(share=args.share, server_port=args.port, allowed_paths=[str(get_output_dir())])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def main():
|
|
188
|
+
"""CLI entry point."""
|
|
189
|
+
parser = argparse.ArgumentParser(
|
|
190
|
+
prog="dedrive",
|
|
191
|
+
description="Google Drive Deduplication Manager",
|
|
192
|
+
)
|
|
193
|
+
parser.add_argument(
|
|
194
|
+
"--profile", "-P",
|
|
195
|
+
help="Use a named profile",
|
|
196
|
+
)
|
|
197
|
+
parser.add_argument(
|
|
198
|
+
"--list-profiles",
|
|
199
|
+
action="store_true",
|
|
200
|
+
help="List available profiles and exit",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
204
|
+
|
|
205
|
+
# login subcommand
|
|
206
|
+
login_parser = subparsers.add_parser("login", help="Authenticate with Google (opens browser)")
|
|
207
|
+
login_parser.add_argument("--profile", "-P", default=argparse.SUPPRESS, help="Use a named profile")
|
|
208
|
+
|
|
209
|
+
# logout subcommand
|
|
210
|
+
logout_parser = subparsers.add_parser("logout", help="Remove saved authentication token")
|
|
211
|
+
logout_parser.add_argument("--profile", "-P", default=argparse.SUPPRESS, help="Use a named profile")
|
|
212
|
+
|
|
213
|
+
# UI flags (only apply when no subcommand)
|
|
214
|
+
parser.add_argument(
|
|
215
|
+
"--validate",
|
|
216
|
+
action="store_true",
|
|
217
|
+
help="Validate credentials and exit without launching the UI",
|
|
218
|
+
)
|
|
219
|
+
parser.add_argument(
|
|
220
|
+
"--verbose", "-v",
|
|
221
|
+
action="store_true",
|
|
222
|
+
help="Enable verbose/debug logging",
|
|
223
|
+
)
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
"--log-file",
|
|
226
|
+
help="Write logs to file (in addition to console)",
|
|
227
|
+
)
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
"--port",
|
|
230
|
+
type=int,
|
|
231
|
+
default=7860,
|
|
232
|
+
help="Gradio server port (default: 7860)",
|
|
233
|
+
)
|
|
234
|
+
parser.add_argument(
|
|
235
|
+
"--share",
|
|
236
|
+
action="store_true",
|
|
237
|
+
help="Enable Gradio public sharing link",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
args = parser.parse_args()
|
|
241
|
+
|
|
242
|
+
# Handle --list-profiles
|
|
243
|
+
if args.list_profiles:
|
|
244
|
+
cmd_list_profiles(args)
|
|
245
|
+
sys.exit(0)
|
|
246
|
+
|
|
247
|
+
# Handle subcommands
|
|
248
|
+
if args.command == "login":
|
|
249
|
+
if args.profile:
|
|
250
|
+
set_active_profile(args.profile)
|
|
251
|
+
cmd_login(args)
|
|
252
|
+
elif args.command == "logout":
|
|
253
|
+
cmd_logout(args)
|
|
254
|
+
else:
|
|
255
|
+
cmd_ui(args)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
main()
|
dedrive/config.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Configuration module for Google Drive Deduplication Tool."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
from dedrive.profiles import load_profile, get_profile_token_path, get_profile_output_dir, init_profile, PROFILES_DIR
|
|
11
|
+
|
|
12
|
+
# Load .env file if present
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
# Active profile (set via set_active_profile)
|
|
16
|
+
active_profile: str | None = None
|
|
17
|
+
_profile_config: dict = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_active_profile(name: str):
|
|
21
|
+
"""Activate a profile, loading its config.yaml."""
|
|
22
|
+
global active_profile, _profile_config
|
|
23
|
+
active_profile = name
|
|
24
|
+
_profile_config = load_profile(name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def set_active_profile_from_email(email: str) -> str:
|
|
28
|
+
"""Create (if needed) and activate a profile based on user email.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
email: The user's email address, used as the profile name.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The profile name (same as email).
|
|
35
|
+
"""
|
|
36
|
+
init_profile(email)
|
|
37
|
+
set_active_profile(email)
|
|
38
|
+
return email
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Default configuration values
|
|
42
|
+
DEFAULTS = {
|
|
43
|
+
"credentials_path": "credentials.json",
|
|
44
|
+
"token_path": "token.json",
|
|
45
|
+
"output_dir": ".output",
|
|
46
|
+
"dupes_folder": "/_dupes",
|
|
47
|
+
"batch_size": 100,
|
|
48
|
+
"max_preview_mb": 10,
|
|
49
|
+
"exclude_paths": [],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Environment variable names
|
|
53
|
+
ENV_VARS = {
|
|
54
|
+
"credentials_path": "GDRIVE_CREDENTIALS_PATH",
|
|
55
|
+
"token_path": "GDRIVE_TOKEN_PATH",
|
|
56
|
+
"output_dir": "GDRIVE_OUTPUT_DIR",
|
|
57
|
+
"dupes_folder": "GDRIVE_DUPES_FOLDER",
|
|
58
|
+
"batch_size": "GDRIVE_BATCH_SIZE",
|
|
59
|
+
"max_preview_mb": "GDRIVE_MAX_PREVIEW_MB",
|
|
60
|
+
"exclude_paths": "GDRIVE_EXCLUDE_PATHS",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
CONFIG_FILE = "config.json"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def expand_path(path: str) -> Path:
|
|
67
|
+
"""Expand ~ and environment variables in path."""
|
|
68
|
+
return Path(os.path.expanduser(os.path.expandvars(path)))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_config() -> dict:
|
|
72
|
+
"""Load configuration from config file if it exists.
|
|
73
|
+
|
|
74
|
+
Checks cwd config.json first, then falls back to ~/.dedrive/config.json.
|
|
75
|
+
"""
|
|
76
|
+
for config_path in [Path(CONFIG_FILE), PROFILES_DIR / CONFIG_FILE]:
|
|
77
|
+
if config_path.exists():
|
|
78
|
+
try:
|
|
79
|
+
with open(config_path) as f:
|
|
80
|
+
return json.load(f)
|
|
81
|
+
except json.JSONDecodeError as e:
|
|
82
|
+
print(f"Error: Invalid JSON in {config_path}: {e}")
|
|
83
|
+
print(f"Please fix the syntax in {config_path} or delete it to use defaults.")
|
|
84
|
+
except PermissionError:
|
|
85
|
+
print(f"Error: Cannot read {config_path} - permission denied.")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(f"Warning: Failed to load {config_path}: {e}")
|
|
88
|
+
return {}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_config_value(key: str, cli_value: Any = None) -> Any:
|
|
92
|
+
"""Get configuration value with precedence: CLI > ENV > config file > default.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
key: Configuration key (e.g., 'credentials_path')
|
|
96
|
+
cli_value: Value from CLI argument (highest precedence if not None)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Configuration value from highest precedence source.
|
|
100
|
+
"""
|
|
101
|
+
# CLI argument has highest precedence
|
|
102
|
+
if cli_value is not None:
|
|
103
|
+
return cli_value
|
|
104
|
+
|
|
105
|
+
# Profile config.yaml (when a profile is active)
|
|
106
|
+
if active_profile and key in _profile_config:
|
|
107
|
+
return _profile_config[key]
|
|
108
|
+
|
|
109
|
+
# Environment variable
|
|
110
|
+
env_var = ENV_VARS.get(key)
|
|
111
|
+
if env_var:
|
|
112
|
+
env_value = os.environ.get(env_var)
|
|
113
|
+
if env_value is not None:
|
|
114
|
+
# Handle type conversion
|
|
115
|
+
if key == "batch_size":
|
|
116
|
+
try:
|
|
117
|
+
return int(env_value)
|
|
118
|
+
except ValueError:
|
|
119
|
+
print(f"Warning: Invalid {env_var} value '{env_value}', using default.")
|
|
120
|
+
elif key == "max_preview_mb":
|
|
121
|
+
try:
|
|
122
|
+
return int(env_value)
|
|
123
|
+
except ValueError:
|
|
124
|
+
print(f"Warning: Invalid {env_var} value '{env_value}', using default.")
|
|
125
|
+
else:
|
|
126
|
+
return env_value
|
|
127
|
+
|
|
128
|
+
# Config file
|
|
129
|
+
config = load_config()
|
|
130
|
+
if key in config:
|
|
131
|
+
return config[key]
|
|
132
|
+
|
|
133
|
+
# Default
|
|
134
|
+
return DEFAULTS.get(key)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_credentials_path(cli_value: str = None) -> Path:
|
|
138
|
+
"""Get credentials file path.
|
|
139
|
+
|
|
140
|
+
Falls back to ~/.dedrive/credentials.json when the default
|
|
141
|
+
credentials.json doesn't exist in cwd.
|
|
142
|
+
"""
|
|
143
|
+
path = get_config_value("credentials_path", cli_value)
|
|
144
|
+
resolved = expand_path(path)
|
|
145
|
+
if not resolved.exists() and path == DEFAULTS["credentials_path"]:
|
|
146
|
+
fallback = PROFILES_DIR / "credentials.json"
|
|
147
|
+
if fallback.exists():
|
|
148
|
+
return fallback
|
|
149
|
+
return resolved
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_token_path(credentials_path: Path = None) -> Path:
|
|
153
|
+
"""Get token file path.
|
|
154
|
+
|
|
155
|
+
By default, token.json is stored next to credentials.json.
|
|
156
|
+
Can be overridden via GDRIVE_TOKEN_PATH or config file.
|
|
157
|
+
"""
|
|
158
|
+
if active_profile:
|
|
159
|
+
return get_profile_token_path(active_profile)
|
|
160
|
+
|
|
161
|
+
explicit_path = get_config_value("token_path")
|
|
162
|
+
|
|
163
|
+
# If explicitly set (not default), use that
|
|
164
|
+
if explicit_path != DEFAULTS["token_path"]:
|
|
165
|
+
return expand_path(explicit_path)
|
|
166
|
+
|
|
167
|
+
# Otherwise, store next to credentials file
|
|
168
|
+
if credentials_path:
|
|
169
|
+
return credentials_path.parent / "token.json"
|
|
170
|
+
|
|
171
|
+
return Path("token.json")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_output_dir() -> Path:
|
|
175
|
+
"""Get output directory path."""
|
|
176
|
+
if active_profile:
|
|
177
|
+
return get_profile_output_dir(active_profile)
|
|
178
|
+
path = get_config_value("output_dir")
|
|
179
|
+
return expand_path(path)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_dupes_folder() -> str:
|
|
183
|
+
"""Get the name of the dupes folder in Google Drive."""
|
|
184
|
+
return get_config_value("dupes_folder")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_batch_size() -> int:
|
|
188
|
+
"""Get batch size for API operations."""
|
|
189
|
+
return get_config_value("batch_size")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_max_preview_size() -> int:
|
|
193
|
+
"""Get max preview size in bytes."""
|
|
194
|
+
mb = get_config_value("max_preview_mb")
|
|
195
|
+
return mb * 1024 * 1024
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_exclude_paths(cli_excludes: list[str] = None) -> list[str]:
|
|
199
|
+
"""Get exclude paths from CLI, config file, and environment variable.
|
|
200
|
+
|
|
201
|
+
Sources (combined):
|
|
202
|
+
1. CLI arguments (--exclude flags)
|
|
203
|
+
2. Config file: config.json with "exclude_paths" array
|
|
204
|
+
3. Environment variable: GDRIVE_EXCLUDE_PATHS (comma-separated paths)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List of paths to exclude from scans.
|
|
208
|
+
"""
|
|
209
|
+
exclude_paths = []
|
|
210
|
+
|
|
211
|
+
# CLI arguments
|
|
212
|
+
if cli_excludes:
|
|
213
|
+
exclude_paths.extend(cli_excludes)
|
|
214
|
+
|
|
215
|
+
# Profile config.yaml (when a profile is active)
|
|
216
|
+
if active_profile and "exclude_paths" in _profile_config:
|
|
217
|
+
profile_paths = _profile_config["exclude_paths"]
|
|
218
|
+
if isinstance(profile_paths, list):
|
|
219
|
+
exclude_paths.extend(profile_paths)
|
|
220
|
+
|
|
221
|
+
# Load from config file
|
|
222
|
+
config = load_config()
|
|
223
|
+
file_paths = config.get("exclude_paths", [])
|
|
224
|
+
if isinstance(file_paths, list):
|
|
225
|
+
exclude_paths.extend(file_paths)
|
|
226
|
+
|
|
227
|
+
# Load from environment variable (comma-separated)
|
|
228
|
+
env_var = ENV_VARS.get("exclude_paths")
|
|
229
|
+
env_paths = os.environ.get(env_var, "")
|
|
230
|
+
if env_paths:
|
|
231
|
+
for path in env_paths.split(","):
|
|
232
|
+
path = path.strip()
|
|
233
|
+
if path:
|
|
234
|
+
exclude_paths.append(path)
|
|
235
|
+
|
|
236
|
+
# Normalize paths (ensure they start with / and don't end with /)
|
|
237
|
+
normalized = []
|
|
238
|
+
for path in exclude_paths:
|
|
239
|
+
path = path.strip()
|
|
240
|
+
# Skip comment lines in config
|
|
241
|
+
if path.startswith("#"):
|
|
242
|
+
continue
|
|
243
|
+
if not path.startswith("/"):
|
|
244
|
+
path = "/" + path
|
|
245
|
+
path = path.rstrip("/")
|
|
246
|
+
if path:
|
|
247
|
+
normalized.append(path)
|
|
248
|
+
|
|
249
|
+
return list(set(normalized)) # Remove duplicates
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def create_default_config():
|
|
253
|
+
"""Create a default config file with all available options."""
|
|
254
|
+
default_config = {
|
|
255
|
+
"# credentials_path": "~/.config/dedrive/credentials.json",
|
|
256
|
+
"# token_path": "~/.config/dedrive/token.json",
|
|
257
|
+
"# output_dir": ".output",
|
|
258
|
+
"# dupes_folder": "/_dupes",
|
|
259
|
+
"# batch_size": 100,
|
|
260
|
+
"# max_preview_mb": 10,
|
|
261
|
+
"exclude_paths": [
|
|
262
|
+
"# Add paths to exclude from scans, e.g.:",
|
|
263
|
+
"# /Backup/Old",
|
|
264
|
+
"# /tmp"
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
config_path = Path(CONFIG_FILE)
|
|
269
|
+
if not config_path.exists():
|
|
270
|
+
with open(config_path, "w") as f:
|
|
271
|
+
json.dump(default_config, f, indent=2)
|
|
272
|
+
print(f"Created default config file: {CONFIG_FILE}")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def print_config():
|
|
276
|
+
"""Print current configuration for debugging."""
|
|
277
|
+
print("Current configuration:")
|
|
278
|
+
print(f" credentials_path: {get_credentials_path()}")
|
|
279
|
+
print(f" token_path: {get_token_path(get_credentials_path())}")
|
|
280
|
+
print(f" output_dir: {get_output_dir()}")
|
|
281
|
+
print(f" dupes_folder: {get_dupes_folder()}")
|
|
282
|
+
print(f" batch_size: {get_batch_size()}")
|
|
283
|
+
print(f" max_preview_mb: {get_config_value('max_preview_mb')}")
|
|
284
|
+
print(f" exclude_paths: {get_exclude_paths()}")
|