-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup.py
More file actions
executable file
·443 lines (347 loc) · 18.1 KB
/
setup.py
File metadata and controls
executable file
·443 lines (347 loc) · 18.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
#!/usr/bin/env python3
"""
Setup script for Yandex Disk KDE Dolphin integration - Python version only
Handles installation and configuration of the Python implementation
"""
import os
import sys
import shutil
import subprocess
from pathlib import Path
import click
try:
from dotenv import dotenv_values
DOTENV_AVAILABLE = True
except ImportError:
DOTENV_AVAILABLE = False
class YandexDiskSetup:
# File and directory name constants
DEFAULT_YA_DISK_RELATIVE = "yaDisk"
DEFAULT_INBOX_RELATIVE = "Media"
LOG_FILE_NAME = "yaMedia-python.log"
ENV_FILE_NAME = ".env"
TEMPLATE_FILE_NAME = "ydpublish-python.desktop.template"
DESKTOP_FILE_NAME = "ydpublish-python.desktop"
PYTHON_SCRIPT_NAME = "ydmenu.py"
PYTHON_WRAPPER_NAME = "ydmenu-py-env"
PROFILE_FILE_NAME = ".profile"
TEMP_ENV_FILE_NAME = "environment_temp"
# Path constants
DEFAULT_YA_DISK_ROOT = "~/Public"
SYSTEM_ENV_FILE_PATH = "/etc/environment"
TMP_DIR = "/tmp"
BIN_DIR_NAME = "bin"
# Service menu directory paths
KSERVICES5_PATH = ".local/share/kservices5/ServiceMenus"
KIO_SERVICEMENUS_PATH = ".local/share/kio/servicemenus"
# Environment variable names
ENV_VAR_YADISK_MENU_VERSION = "YADISK_MENU_VERSION"
ENV_VAR_YA_DISK_ROOT = "YA_DISK_ROOT"
# Default values
DEFAULT_VERSION = "Unknown"
FILE_PERMISSIONS = 0o755
# Exit codes
EXIT_CODE_ERROR = 1
# Required system commands (base)
REQUIRED_COMMANDS = ['yandex-disk']
# Optional commands (at least one required from each group)
OPTIONAL_COMMANDS = {
'dialog': ['kdialog', 'notify-send'], # KDE or GNOME notification
'clipboard': ['xclip', 'wl-copy', 'wl-paste'] # X11 or Wayland clipboard
}
# Setup messages
SETUP_PREFIX = "[SETUP]"
# PATH export line for bashrc
BIN_PATH_EXPORT = 'PATH="$HOME/bin:$PATH"'
PATH_COMMENT = "# Added by ydmenu setup"
# Setup completion messages
NEXT_STEPS = [
"1. Restart your session or run: source /etc/environment",
"2. Make sure yandex-disk daemon is running",
"3. Test the integration by right-clicking a file in Dolphin"
]
def __init__(self):
# Default configuration values
self.ya_disk_root = os.path.expanduser(self.DEFAULT_YA_DISK_ROOT)
self.ya_disk_relative = self.DEFAULT_YA_DISK_RELATIVE
self.inbox_relative = self.DEFAULT_INBOX_RELATIVE
self.log_path = f"$YA_DISK_ROOT/{self.LOG_FILE_NAME}"
self.default_version = self.DEFAULT_VERSION
# Paths
self.script_dir = Path(__file__).parent.absolute()
self.env_file = Path(self.SYSTEM_ENV_FILE_PATH)
# Service menu directories
self.service_menu_dirs = [
Path.home() / self.KSERVICES5_PATH,
Path.home() / self.KIO_SERVICEMENUS_PATH
]
self.bin_dir = Path.home() / self.BIN_DIR_NAME
def print_status(self, message: str) -> None:
"""Print status message"""
print(f"{self.SETUP_PREFIX} {message}")
def extract_version_from_env(self) -> str:
"""Extract version from .env file"""
if not DOTENV_AVAILABLE:
raise ImportError("python-dotenv is required but not available. Install it with: pip install python-dotenv")
env_file = self.script_dir / self.ENV_FILE_NAME
if not env_file.exists():
raise FileNotFoundError(f"{self.ENV_FILE_NAME} file not found at {env_file}")
try:
# Use dotenv to parse the .env file
env_vars = dotenv_values(env_file)
version = env_vars.get(self.ENV_VAR_YADISK_MENU_VERSION)
if version:
self.print_status(f"Extracted version from .env: {version}")
return version
else:
self.print_status(f"Warning: {self.ENV_VAR_YADISK_MENU_VERSION} not found in {self.ENV_FILE_NAME} file, use default value {self.default_version}")
return self.default_version
except Exception as e:
self.print_status(f"Error reading version from {self.ENV_FILE_NAME} file: {e}, use default value {self.default_version}")
return self.default_version
def generate_desktop_file(self) -> None:
"""Generate desktop file from template with version"""
self.print_status("Generating desktop file with version")
template_file = self.script_dir / self.TEMPLATE_FILE_NAME
desktop_file = self.script_dir / self.DESKTOP_FILE_NAME
if not template_file.exists():
self.print_status(f"Warning: Template file not found: {template_file}")
return
try:
# Extract version from .env file
version = self.extract_version_from_env()
# Read template
with open(template_file, 'r') as f:
template_content = f.read()
# Replace version placeholder
desktop_content = template_content.replace('@@VERSION@@', f'v{version}')
# Write desktop file
with open(desktop_file, 'w') as f:
f.write(desktop_content)
self.print_status(f"Generated desktop file with version v{version}")
except Exception as e:
self.print_status(f"Error generating desktop file: {e}")
return
def set_global_environment_variable(self) -> None:
"""Set global YA_DISK_ROOT environment variable in /etc/environment"""
self.print_status("Setting global YA_DISK_ROOT environment variable")
self.print_status("Root access required to set global static YA_DISK_ROOT var")
try:
# Read current environment file
env_lines = []
if self.env_file.exists():
with open(self.env_file, 'r') as f:
env_lines = f.readlines()
# Check if YA_DISK_ROOT already exists
ya_disk_line = f'{self.ENV_VAR_YA_DISK_ROOT}="{self.ya_disk_root}"\n'
found_existing = False
# Update existing or prepare to add new
for i, line in enumerate(env_lines):
if line.strip().startswith(f'{self.ENV_VAR_YA_DISK_ROOT}='):
env_lines[i] = ya_disk_line
found_existing = True
break
if not found_existing:
env_lines.append(ya_disk_line)
# Write back to file using sudo
temp_env_file = f"{self.TMP_DIR}/{self.TEMP_ENV_FILE_NAME}"
with open(temp_env_file, 'w') as f:
f.writelines(env_lines)
# Use sudo to copy the temporary file to /etc/environment
subprocess.run(['sudo', 'cp', temp_env_file, str(self.env_file)], check=True)
os.remove(temp_env_file) # Clean up temp file
self.print_status(f"{self.ENV_VAR_YA_DISK_ROOT} set to: {self.ya_disk_root}")
except (subprocess.CalledProcessError, IOError) as e:
self.print_status(f"Error setting environment variable: {e}")
sys.exit(self.EXIT_CODE_ERROR)
def update_script_variables(self) -> None:
"""Update variables in ydmenu.py script"""
self.print_status("Set local script-scoped vars")
ydmenu_file = self.script_dir / self.PYTHON_SCRIPT_NAME
if not ydmenu_file.exists():
self.print_status(f"Error: {ydmenu_file} not found")
return
# Update ydmenu.py (Python script already uses environment variables, no changes needed)
self.print_status("Python script already configured to use environment variables")
self.print_status("Python version uses ydpublish-python.desktop (no modifications needed)")
def create_symlinks(self, skip_kde: bool = False) -> None:
"""Create symlinks in KDE service menu directories and bin"""
self.print_status("Create symlinks accordingly")
# Ensure bin directory exists
self.bin_dir.mkdir(exist_ok=True)
# Create symlinks for KDE service menus (unless skipped)
desktop_files = [(self.DESKTOP_FILE_NAME, self.DESKTOP_FILE_NAME)] # Python version
if not skip_kde:
for service_menu_dir in self.service_menu_dirs:
service_menu_dir.mkdir(parents=True, exist_ok=True)
for desktop_file, link_name in desktop_files:
desktop_link = service_menu_dir / link_name
desktop_source = self.script_dir / desktop_file
if not desktop_source.exists():
self.print_status(f"Source file not found: {desktop_source}")
continue
if not desktop_link.is_symlink():
# Backup existing file if it exists and is not a symlink
if desktop_link.exists():
backup_file = service_menu_dir / f"{link_name}.bak"
if backup_file.exists():
self.print_status(f"Backup already exists: {backup_file}")
else:
self.print_status(f"Create backup for default desktop file: {backup_file}")
shutil.move(str(desktop_link), str(backup_file))
# Create symlink (remove existing file first if needed)
if desktop_link.exists():
desktop_link.unlink()
desktop_link.symlink_to(desktop_source)
self.print_status(f"Created symlink: {desktop_link} -> {desktop_source}")
else:
self.print_status("Skipping KDE service menu installation (--skip-kde flag)")
# Make desktop files executable
for desktop_file, _ in desktop_files:
desktop_path = self.script_dir / desktop_file
if desktop_path.exists():
desktop_path.chmod(self.FILE_PERMISSIONS)
self.print_status(f"Made executable: {desktop_path}")
# Python setup handles only Python-related files
# Create symlinks for Python script in bin directory
script_links = [
(self.PYTHON_SCRIPT_NAME, self.PYTHON_SCRIPT_NAME), # Python script
]
for script_file, link_name in script_links:
script_link = self.bin_dir / link_name
script_source = self.script_dir / script_file
if script_source.exists() and not script_link.is_symlink() and not script_link.exists():
script_link.symlink_to(script_source)
self.print_status(f"Created symlink: {script_link} -> {script_source}")
# Ensure the ydmenu-py-env wrapper script exists, it should be already executable, it is done in make install phase
python_wrapper_source = self.script_dir / self.PYTHON_WRAPPER_NAME
if not python_wrapper_source.exists():
self.print_status(f"Warning: {python_wrapper_source} not found")
# Create symlink in user's ~/bin
user_bin_python = Path.home() / self.BIN_DIR_NAME / self.PYTHON_WRAPPER_NAME
Path.home().mkdir(exist_ok=True)
(Path.home() / self.BIN_DIR_NAME).mkdir(exist_ok=True)
if user_bin_python.is_symlink() or user_bin_python.exists():
user_bin_python.unlink()
if python_wrapper_source.exists():
user_bin_python.symlink_to(python_wrapper_source)
self.print_status(f"Created symlink: {user_bin_python} -> {python_wrapper_source}")
# Also ensure ~/bin is in PATH (add to ~/.profile if not present)
profile_path = Path.home() / self.PROFILE_FILE_NAME
if profile_path.exists():
profile_content = profile_path.read_text()
if self.BIN_PATH_EXPORT not in profile_content:
with open(profile_path, 'a') as f:
f.write(f'\n{self.PATH_COMMENT}\n{self.BIN_PATH_EXPORT}\n')
self.print_status("Added ~/bin to PATH in ~/.profile")
def create_directories(self) -> None:
"""Create necessary Yandex Disk directories"""
self.print_status("Creating Yandex Disk directories")
ya_disk_path = Path(self.ya_disk_root) / self.ya_disk_relative
stream_path = ya_disk_path / self.inbox_relative
ya_disk_path.mkdir(parents=True, exist_ok=True)
stream_path.mkdir(parents=True, exist_ok=True)
self.print_status(f"Created directories: {ya_disk_path}, {stream_path}")
def setup_virtual_environment(self) -> None:
"""Setup Python virtual environment and install dependencies"""
self.print_status("Setting up Python virtual environment")
venv_dir = self.script_dir / "venv"
requirements_file = self.script_dir / "requirements.txt"
if not venv_dir.exists():
subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
self.print_status("Created virtual environment")
if requirements_file.exists():
pip_path = venv_dir / "bin" / "pip"
subprocess.run([str(pip_path), "install", "-r", str(requirements_file)], check=True)
self.print_status("Installed Python dependencies")
def verify_dependencies(self) -> bool:
"""Verify system dependencies are available"""
self.print_status("Verifying system dependencies")
# Check required commands
missing_required = []
for cmd in self.REQUIRED_COMMANDS:
if not shutil.which(cmd):
missing_required.append(cmd)
# Check optional command groups (at least one from each group)
missing_groups = {}
for group_name, commands in self.OPTIONAL_COMMANDS.items():
available = [cmd for cmd in commands if shutil.which(cmd)]
if not available:
missing_groups[group_name] = commands
# Report issues
if missing_required or missing_groups:
if missing_required:
self.print_status(f"Missing required commands: {', '.join(missing_required)}")
if missing_groups:
for group_name, commands in missing_groups.items():
self.print_status(f"Missing {group_name} tools (need at least one): {', '.join(commands)}")
self.print_status("Please install the missing dependencies:")
self.print_status("- yandex-disk: Yandex Disk daemon")
if 'dialog' in missing_groups:
self.print_status("- kdialog (KDE) OR notify-send (GNOME): Notification utility")
if 'clipboard' in missing_groups:
# Detect session type for better guidance
import os
if os.environ.get('WAYLAND_DISPLAY') or os.environ.get('XDG_SESSION_TYPE') == 'wayland':
self.print_status("- wl-clipboard: Wayland clipboard utility")
else:
self.print_status("- xclip: X11 clipboard utility")
return False
self.print_status("All system dependencies are available")
return True
@click.command()
@click.option('--ya-disk-root', default=None, help='Parent directory of Yandex disk')
@click.option('--ya-disk-relative', default='yaDisk', help='Yandex disk directory name')
@click.option('--inbox-relative', default='Media', help='Inbox directory for file stream')
@click.option('--skip-env', is_flag=True, help='Skip environment variable setup')
@click.option('--skip-kde', is_flag=True, help='Skip KDE service menu installation')
@click.option('--check-deps', is_flag=True, help='Only check dependencies')
def main(ya_disk_root: str, ya_disk_relative: str, inbox_relative: str,
skip_env: bool, skip_kde: bool, check_deps: bool):
"""Setup Yandex Disk KDE Dolphin integration - Python version"""
setup = YandexDiskSetup()
# Override defaults with command line options
if ya_disk_root:
setup.ya_disk_root = os.path.expanduser(ya_disk_root)
setup.ya_disk_relative = ya_disk_relative
setup.inbox_relative = inbox_relative
print(f"Working directory: {setup.script_dir}")
print(f"YA_DISK_ROOT: {setup.ya_disk_root}")
print(f"YA_DISK_RELATIVE: {setup.ya_disk_relative}")
print(f"INBOX_RELATIVE: {setup.inbox_relative}")
print()
# Check dependencies first
if not setup.verify_dependencies():
if check_deps:
sys.exit(YandexDiskSetup.EXIT_CODE_ERROR)
else:
print("Warning: Some dependencies are missing. Installation will continue.")
if check_deps:
print("Dependency check completed successfully.")
return
try:
# Setup virtual environment and install Python dependencies
setup.setup_virtual_environment()
# Generate desktop file with version
setup.generate_desktop_file()
# Create necessary directories
setup.create_directories()
# Set global environment variable
if not skip_env:
setup.set_global_environment_variable()
else:
setup.print_status("Skipping environment variable setup")
# Update script variables
setup.update_script_variables()
# Create symlinks
setup.create_symlinks(skip_kde)
setup.print_status("Setup completed successfully!")
setup.print_status("")
setup.print_status("Next steps:")
for step in setup.NEXT_STEPS:
setup.print_status(step)
except Exception as e:
setup.print_status(f"Setup failed: {e}")
sys.exit(YandexDiskSetup.EXIT_CODE_ERROR)
if __name__ == '__main__':
main()