spatelier 0.3.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.
- analytics/__init__.py +1 -0
- analytics/reporter.py +497 -0
- cli/__init__.py +1 -0
- cli/app.py +147 -0
- cli/audio.py +129 -0
- cli/cli_analytics.py +320 -0
- cli/cli_utils.py +282 -0
- cli/error_handlers.py +122 -0
- cli/files.py +299 -0
- cli/update.py +325 -0
- cli/video.py +823 -0
- cli/worker.py +615 -0
- core/__init__.py +1 -0
- core/analytics_dashboard.py +368 -0
- core/base.py +303 -0
- core/base_service.py +69 -0
- core/config.py +345 -0
- core/database_service.py +116 -0
- core/decorators.py +263 -0
- core/error_handler.py +210 -0
- core/file_tracker.py +254 -0
- core/interactive_cli.py +366 -0
- core/interfaces.py +166 -0
- core/job_queue.py +437 -0
- core/logger.py +79 -0
- core/package_updater.py +469 -0
- core/progress.py +228 -0
- core/service_factory.py +295 -0
- core/streaming.py +299 -0
- core/worker.py +765 -0
- database/__init__.py +1 -0
- database/connection.py +265 -0
- database/metadata.py +516 -0
- database/models.py +288 -0
- database/repository.py +592 -0
- database/transcription_storage.py +219 -0
- modules/__init__.py +1 -0
- modules/audio/__init__.py +5 -0
- modules/audio/converter.py +197 -0
- modules/video/__init__.py +16 -0
- modules/video/converter.py +191 -0
- modules/video/fallback_extractor.py +334 -0
- modules/video/services/__init__.py +18 -0
- modules/video/services/audio_extraction_service.py +274 -0
- modules/video/services/download_service.py +852 -0
- modules/video/services/metadata_service.py +190 -0
- modules/video/services/playlist_service.py +445 -0
- modules/video/services/transcription_service.py +491 -0
- modules/video/transcription_service.py +385 -0
- modules/video/youtube_api.py +397 -0
- spatelier/__init__.py +33 -0
- spatelier-0.3.0.dist-info/METADATA +260 -0
- spatelier-0.3.0.dist-info/RECORD +59 -0
- spatelier-0.3.0.dist-info/WHEEL +5 -0
- spatelier-0.3.0.dist-info/entry_points.txt +2 -0
- spatelier-0.3.0.dist-info/licenses/LICENSE +21 -0
- spatelier-0.3.0.dist-info/top_level.txt +7 -0
- utils/__init__.py +1 -0
- utils/helpers.py +250 -0
cli/cli_utils.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility CLI commands.
|
|
3
|
+
|
|
4
|
+
This module provides command-line interfaces for utility operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from core.config import Config
|
|
16
|
+
from core.decorators import handle_errors, time_operation
|
|
17
|
+
from core.logger import get_logger
|
|
18
|
+
from utils.helpers import format_file_size, get_file_hash, get_file_size, get_file_type
|
|
19
|
+
|
|
20
|
+
# Create the utils CLI app
|
|
21
|
+
app = typer.Typer(
|
|
22
|
+
name="utils",
|
|
23
|
+
help="Utility commands",
|
|
24
|
+
rich_markup_mode="rich",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command()
|
|
31
|
+
def hash(
|
|
32
|
+
file_path: Path = typer.Argument(..., help="File to hash"),
|
|
33
|
+
algorithm: str = typer.Option("sha256", "--algorithm", "-a", help="Hash algorithm"),
|
|
34
|
+
verbose: bool = typer.Option(
|
|
35
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
36
|
+
),
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Calculate hash of a file.
|
|
40
|
+
"""
|
|
41
|
+
config = Config()
|
|
42
|
+
logger = get_logger("utils-hash", verbose=verbose)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
if not file_path.exists():
|
|
46
|
+
console.print(
|
|
47
|
+
Panel(
|
|
48
|
+
f"[red]✗[/red] File not found: {file_path}",
|
|
49
|
+
title="Error",
|
|
50
|
+
border_style="red",
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
raise typer.Exit(1)
|
|
54
|
+
|
|
55
|
+
hash_value = get_file_hash(file_path, algorithm)
|
|
56
|
+
|
|
57
|
+
console.print(
|
|
58
|
+
Panel(
|
|
59
|
+
f"File: {file_path}\n"
|
|
60
|
+
f"Algorithm: {algorithm.upper()}\n"
|
|
61
|
+
f"Hash: {hash_value}",
|
|
62
|
+
title="File Hash",
|
|
63
|
+
border_style="green",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Hash calculation failed: {e}")
|
|
69
|
+
console.print(
|
|
70
|
+
Panel(
|
|
71
|
+
f"[red]✗[/red] Hash calculation failed: {str(e)}",
|
|
72
|
+
title="Error",
|
|
73
|
+
border_style="red",
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
raise typer.Exit(1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command()
|
|
80
|
+
def info(
|
|
81
|
+
file_path: Path = typer.Argument(..., help="File to analyze"),
|
|
82
|
+
verbose: bool = typer.Option(
|
|
83
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
84
|
+
),
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
Display detailed information about a file.
|
|
88
|
+
"""
|
|
89
|
+
config = Config()
|
|
90
|
+
logger = get_logger("utils-info", verbose=verbose)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
if not file_path.exists():
|
|
94
|
+
console.print(
|
|
95
|
+
Panel(
|
|
96
|
+
f"[red]✗[/red] File not found: {file_path}",
|
|
97
|
+
title="Error",
|
|
98
|
+
border_style="red",
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
# Get file information
|
|
104
|
+
file_size = get_file_size(file_path)
|
|
105
|
+
file_type = get_file_type(file_path)
|
|
106
|
+
file_hash = get_file_hash(file_path)
|
|
107
|
+
|
|
108
|
+
# Create info table
|
|
109
|
+
table = Table(title=f"File Information: {file_path.name}")
|
|
110
|
+
table.add_column("Property", style="cyan")
|
|
111
|
+
table.add_column("Value", style="magenta")
|
|
112
|
+
|
|
113
|
+
table.add_row("File Path", str(file_path))
|
|
114
|
+
table.add_row("File Name", file_path.name)
|
|
115
|
+
table.add_row("File Size", format_file_size(file_size))
|
|
116
|
+
table.add_row("File Type", file_type)
|
|
117
|
+
table.add_row("Extension", file_path.suffix)
|
|
118
|
+
table.add_row("SHA256", file_hash)
|
|
119
|
+
|
|
120
|
+
console.print(table)
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f"File analysis failed: {e}")
|
|
124
|
+
console.print(
|
|
125
|
+
Panel(
|
|
126
|
+
f"[red]✗[/red] File analysis failed: {str(e)}",
|
|
127
|
+
title="Error",
|
|
128
|
+
border_style="red",
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
raise typer.Exit(1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.command()
|
|
135
|
+
def find(
|
|
136
|
+
directory: Path = typer.Argument(..., help="Directory to search"),
|
|
137
|
+
pattern: str = typer.Option("*", "--pattern", "-p", help="File pattern to match"),
|
|
138
|
+
file_types: Optional[List[str]] = typer.Option(
|
|
139
|
+
None, "--type", "-t", help="File types to filter by"
|
|
140
|
+
),
|
|
141
|
+
recursive: bool = typer.Option(
|
|
142
|
+
True, "--recursive", "-r", help="Search recursively"
|
|
143
|
+
),
|
|
144
|
+
verbose: bool = typer.Option(
|
|
145
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
146
|
+
),
|
|
147
|
+
):
|
|
148
|
+
"""
|
|
149
|
+
Find files matching pattern in directory.
|
|
150
|
+
"""
|
|
151
|
+
config = Config()
|
|
152
|
+
logger = get_logger("utils-find", verbose=verbose)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
if not directory.exists():
|
|
156
|
+
console.print(
|
|
157
|
+
Panel(
|
|
158
|
+
f"[red]✗[/red] Directory not found: {directory}",
|
|
159
|
+
title="Error",
|
|
160
|
+
border_style="red",
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
raise typer.Exit(1)
|
|
164
|
+
|
|
165
|
+
from utils.helpers import find_files
|
|
166
|
+
|
|
167
|
+
files = find_files(directory, pattern, recursive, file_types)
|
|
168
|
+
|
|
169
|
+
if not files:
|
|
170
|
+
console.print(
|
|
171
|
+
Panel(
|
|
172
|
+
f"[yellow]⚠[/yellow] No files found matching pattern: {pattern}",
|
|
173
|
+
title="No Files",
|
|
174
|
+
border_style="yellow",
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Create results table
|
|
180
|
+
table = Table(title=f"Found {len(files)} files")
|
|
181
|
+
table.add_column("File", style="cyan")
|
|
182
|
+
table.add_column("Size", style="magenta")
|
|
183
|
+
table.add_column("Type", style="green")
|
|
184
|
+
|
|
185
|
+
for file_path in files[:50]: # Limit to first 50 results
|
|
186
|
+
file_size = get_file_size(file_path)
|
|
187
|
+
file_type = get_file_type(file_path)
|
|
188
|
+
table.add_row(
|
|
189
|
+
str(file_path.relative_to(directory)),
|
|
190
|
+
format_file_size(file_size),
|
|
191
|
+
file_type,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
console.print(table)
|
|
195
|
+
|
|
196
|
+
if len(files) > 50:
|
|
197
|
+
console.print(f"\n... and {len(files) - 50} more files")
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.error(f"File search failed: {e}")
|
|
201
|
+
console.print(
|
|
202
|
+
Panel(
|
|
203
|
+
f"[red]✗[/red] File search failed: {str(e)}",
|
|
204
|
+
title="Error",
|
|
205
|
+
border_style="red",
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
raise typer.Exit(1)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command()
|
|
212
|
+
def config(
|
|
213
|
+
show: bool = typer.Option(False, "--show", "-s", help="Show current configuration"),
|
|
214
|
+
edit: bool = typer.Option(False, "--edit", "-e", help="Edit configuration file"),
|
|
215
|
+
reset: bool = typer.Option(
|
|
216
|
+
False, "--reset", "-r", help="Reset to default configuration"
|
|
217
|
+
),
|
|
218
|
+
verbose: bool = typer.Option(
|
|
219
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
220
|
+
),
|
|
221
|
+
):
|
|
222
|
+
"""
|
|
223
|
+
Manage configuration settings.
|
|
224
|
+
"""
|
|
225
|
+
config = Config()
|
|
226
|
+
logger = get_logger("utils-config", verbose=verbose)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
if show:
|
|
230
|
+
# Show current configuration
|
|
231
|
+
table = Table(title="Current Configuration")
|
|
232
|
+
table.add_column("Setting", style="cyan")
|
|
233
|
+
table.add_column("Value", style="magenta")
|
|
234
|
+
|
|
235
|
+
table.add_row("Video Format", config.video.default_format)
|
|
236
|
+
table.add_row("Video Quality", config.video.quality)
|
|
237
|
+
table.add_row("Video Output Dir", str(config.video.output_dir))
|
|
238
|
+
table.add_row("Audio Format", config.audio.default_format)
|
|
239
|
+
table.add_row("Audio Bitrate", str(config.audio.bitrate))
|
|
240
|
+
table.add_row("Audio Output Dir", str(config.audio.output_dir))
|
|
241
|
+
table.add_row("Log Level", config.log_level)
|
|
242
|
+
|
|
243
|
+
console.print(table)
|
|
244
|
+
|
|
245
|
+
elif edit:
|
|
246
|
+
# Edit configuration file
|
|
247
|
+
config_path = config.get_default_config_path()
|
|
248
|
+
console.print(
|
|
249
|
+
Panel(
|
|
250
|
+
f"[yellow]⚠[/yellow] Configuration editing not yet implemented.\n"
|
|
251
|
+
f"Config file: {config_path}",
|
|
252
|
+
title="Not Implemented",
|
|
253
|
+
border_style="yellow",
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
elif reset:
|
|
258
|
+
# Reset configuration
|
|
259
|
+
config_path = config.get_default_config_path()
|
|
260
|
+
if config_path.exists():
|
|
261
|
+
config_path.unlink()
|
|
262
|
+
|
|
263
|
+
config.ensure_default_config()
|
|
264
|
+
console.print(
|
|
265
|
+
Panel(
|
|
266
|
+
f"[green]✓[/green] Configuration reset to defaults\n"
|
|
267
|
+
f"Config file: {config_path}",
|
|
268
|
+
title="Reset Complete",
|
|
269
|
+
border_style="green",
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error(f"Configuration management failed: {e}")
|
|
275
|
+
console.print(
|
|
276
|
+
Panel(
|
|
277
|
+
f"[red]✗[/red] Configuration management failed: {str(e)}",
|
|
278
|
+
title="Error",
|
|
279
|
+
border_style="red",
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
raise typer.Exit(1)
|
cli/error_handlers.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common error handlers for CLI commands.
|
|
3
|
+
|
|
4
|
+
This module provides standardized error handling patterns
|
|
5
|
+
for all CLI commands to ensure consistency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def handle_cli_error(
|
|
19
|
+
error: Exception, context: str = "", show_traceback: bool = False
|
|
20
|
+
) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Standardized error handler for CLI commands.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
error: The exception that occurred
|
|
26
|
+
context: Additional context about the operation
|
|
27
|
+
show_traceback: Whether to show full traceback
|
|
28
|
+
"""
|
|
29
|
+
error_msg = str(error)
|
|
30
|
+
|
|
31
|
+
# Create standardized error message
|
|
32
|
+
if context:
|
|
33
|
+
title = f"Error in {context}"
|
|
34
|
+
else:
|
|
35
|
+
title = "Error"
|
|
36
|
+
|
|
37
|
+
# Format error message
|
|
38
|
+
if show_traceback:
|
|
39
|
+
console.print(
|
|
40
|
+
Panel(
|
|
41
|
+
f"[red]✗[/red] {error_msg}\n\nTraceback:\n{error.__traceback__}",
|
|
42
|
+
title=title,
|
|
43
|
+
border_style="red",
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
console.print(
|
|
48
|
+
Panel(f"[red]✗[/red] {error_msg}", title=title, border_style="red")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Exit with error code
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def handle_file_not_found(file_path: Path, operation: str = "access") -> None:
|
|
56
|
+
"""
|
|
57
|
+
Handle file not found errors with consistent messaging.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
file_path: The file that was not found
|
|
61
|
+
operation: The operation being performed (access, read, write, etc.)
|
|
62
|
+
"""
|
|
63
|
+
handle_cli_error(
|
|
64
|
+
FileNotFoundError(f"File not found: {file_path}"), context=f"File {operation}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def handle_directory_not_found(dir_path: Path, operation: str = "access") -> None:
|
|
69
|
+
"""
|
|
70
|
+
Handle directory not found errors with consistent messaging.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
dir_path: The directory that was not found
|
|
74
|
+
operation: The operation being performed (access, read, write, etc.)
|
|
75
|
+
"""
|
|
76
|
+
handle_cli_error(
|
|
77
|
+
FileNotFoundError(f"Directory not found: {dir_path}"),
|
|
78
|
+
context=f"Directory {operation}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def handle_permission_error(file_path: Path, operation: str = "access") -> None:
|
|
83
|
+
"""
|
|
84
|
+
Handle permission errors with consistent messaging.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
file_path: The file/directory with permission issues
|
|
88
|
+
operation: The operation being performed
|
|
89
|
+
"""
|
|
90
|
+
handle_cli_error(
|
|
91
|
+
PermissionError(f"Permission denied: {file_path}"), context=f"File {operation}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def handle_validation_error(message: str, field: str = "") -> None:
|
|
96
|
+
"""
|
|
97
|
+
Handle validation errors with consistent messaging.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
message: The validation error message
|
|
101
|
+
field: The field that failed validation
|
|
102
|
+
"""
|
|
103
|
+
context = f"Validation error for {field}" if field else "Validation error"
|
|
104
|
+
handle_cli_error(ValueError(message), context=context)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def handle_not_implemented(feature: str) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Handle not implemented features with consistent messaging.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
feature: The feature that is not implemented
|
|
113
|
+
"""
|
|
114
|
+
console.print(
|
|
115
|
+
Panel(
|
|
116
|
+
f"[yellow]⚠[/yellow] {feature} is not yet implemented.\n"
|
|
117
|
+
f"This feature is planned for a future release.",
|
|
118
|
+
title="Not Implemented",
|
|
119
|
+
border_style="yellow",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
raise typer.Exit(0) # Exit with success since this is expected
|
cli/files.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File tracking demonstration and CLI commands.
|
|
3
|
+
|
|
4
|
+
This module demonstrates OS-level file tracking capabilities
|
|
5
|
+
and provides CLI commands for file management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from core.config import Config
|
|
18
|
+
from core.file_tracker import FileIdentifier, FileTracker
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
app = typer.Typer(name="files", help="File tracking and management")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command()
|
|
25
|
+
def track(
|
|
26
|
+
file_path: str = typer.Argument(..., help="Path to file to track"),
|
|
27
|
+
verbose: bool = typer.Option(
|
|
28
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
29
|
+
),
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
🔍 Track a file using OS-level identifiers.
|
|
33
|
+
|
|
34
|
+
Shows the OS-level file identifier (device:inode) that persists
|
|
35
|
+
even when files are moved or renamed.
|
|
36
|
+
"""
|
|
37
|
+
config = Config()
|
|
38
|
+
tracker = FileTracker(verbose=verbose)
|
|
39
|
+
|
|
40
|
+
file_path_obj = Path(file_path)
|
|
41
|
+
|
|
42
|
+
if not file_path_obj.exists():
|
|
43
|
+
console.print(
|
|
44
|
+
Panel(
|
|
45
|
+
f"[red]✗[/red] File not found: {file_path}",
|
|
46
|
+
title="Error",
|
|
47
|
+
border_style="red",
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
|
|
52
|
+
# Get file identifier
|
|
53
|
+
file_id = tracker.get_file_identifier(file_path_obj)
|
|
54
|
+
metadata = tracker.get_file_metadata(file_path_obj)
|
|
55
|
+
|
|
56
|
+
if file_id:
|
|
57
|
+
console.print(
|
|
58
|
+
Panel(
|
|
59
|
+
f"[green]✓[/green] File tracked successfully!\n"
|
|
60
|
+
f"File: {metadata['name']}\n"
|
|
61
|
+
f"Path: {metadata['path']}\n"
|
|
62
|
+
f"Size: {metadata['size']:,} bytes\n"
|
|
63
|
+
f"OS Identifier: {file_id}\n"
|
|
64
|
+
f"Device: {metadata['device']}\n"
|
|
65
|
+
f"Inode: {metadata['inode']}\n"
|
|
66
|
+
f"Modified: {metadata['modified']}",
|
|
67
|
+
title="File Tracking Info",
|
|
68
|
+
border_style="green",
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
console.print(
|
|
73
|
+
Panel(
|
|
74
|
+
f"[red]✗[/red] Failed to get file identifier",
|
|
75
|
+
title="Error",
|
|
76
|
+
border_style="red",
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command()
|
|
83
|
+
def find(
|
|
84
|
+
file_id: str = typer.Argument(..., help="File identifier (device:inode)"),
|
|
85
|
+
search_path: Optional[str] = typer.Option(None, "--path", "-p", help="Search path"),
|
|
86
|
+
verbose: bool = typer.Option(
|
|
87
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
88
|
+
),
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
🔍 Find a file by its OS-level identifier.
|
|
92
|
+
|
|
93
|
+
Searches for a file using its device:inode identifier,
|
|
94
|
+
useful when files have been moved.
|
|
95
|
+
"""
|
|
96
|
+
config = Config()
|
|
97
|
+
tracker = FileTracker(verbose=verbose)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
device, inode = file_id.split(":")
|
|
101
|
+
file_id_obj = FileIdentifier(device=int(device), inode=int(inode))
|
|
102
|
+
except ValueError:
|
|
103
|
+
console.print(
|
|
104
|
+
Panel(
|
|
105
|
+
f"[red]✗[/red] Invalid file identifier format. Use 'device:inode' (e.g., '16777234:19668159')",
|
|
106
|
+
title="Error",
|
|
107
|
+
border_style="red",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
raise typer.Exit(1)
|
|
111
|
+
|
|
112
|
+
# Set search paths
|
|
113
|
+
if search_path:
|
|
114
|
+
search_paths = [Path(search_path)]
|
|
115
|
+
else:
|
|
116
|
+
# Default search paths
|
|
117
|
+
search_paths = [
|
|
118
|
+
Path.home(),
|
|
119
|
+
Path("/tmp"),
|
|
120
|
+
Path("/var/tmp"),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# Find the file
|
|
124
|
+
found_path = tracker.find_file_by_identifier(file_id_obj, search_paths)
|
|
125
|
+
|
|
126
|
+
if found_path:
|
|
127
|
+
metadata = tracker.get_file_metadata(found_path)
|
|
128
|
+
console.print(
|
|
129
|
+
Panel(
|
|
130
|
+
f"[green]✓[/green] File found!\n"
|
|
131
|
+
f"File: {metadata['name']}\n"
|
|
132
|
+
f"Path: {metadata['path']}\n"
|
|
133
|
+
f"Size: {metadata['size']:,} bytes\n"
|
|
134
|
+
f"Modified: {metadata['modified']}",
|
|
135
|
+
title="File Found",
|
|
136
|
+
border_style="green",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
console.print(
|
|
141
|
+
Panel(
|
|
142
|
+
f"[red]✗[/red] File with identifier {file_id} was not found in search paths",
|
|
143
|
+
title="File Not Found",
|
|
144
|
+
border_style="red",
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@app.command()
|
|
151
|
+
def duplicates(
|
|
152
|
+
search_path: str = typer.Argument(..., help="Path to search for duplicates"),
|
|
153
|
+
verbose: bool = typer.Option(
|
|
154
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
155
|
+
),
|
|
156
|
+
):
|
|
157
|
+
"""
|
|
158
|
+
🔍 Find duplicate files based on OS-level identifiers.
|
|
159
|
+
|
|
160
|
+
Identifies files that have the same device:inode identifier,
|
|
161
|
+
which means they are hard links to the same file.
|
|
162
|
+
"""
|
|
163
|
+
config = Config()
|
|
164
|
+
tracker = FileTracker(verbose=verbose)
|
|
165
|
+
|
|
166
|
+
search_path_obj = Path(search_path)
|
|
167
|
+
|
|
168
|
+
if not search_path_obj.exists():
|
|
169
|
+
console.print(
|
|
170
|
+
Panel(
|
|
171
|
+
f"[red]✗[/red] Search path not found: {search_path}",
|
|
172
|
+
title="Error",
|
|
173
|
+
border_style="red",
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
raise typer.Exit(1)
|
|
177
|
+
|
|
178
|
+
# Find duplicates
|
|
179
|
+
duplicates = tracker.find_duplicate_files([search_path_obj])
|
|
180
|
+
|
|
181
|
+
if duplicates:
|
|
182
|
+
table = Table(title="Duplicate Files Found")
|
|
183
|
+
table.add_column("File Identifier", style="cyan")
|
|
184
|
+
table.add_column("Count", style="magenta")
|
|
185
|
+
table.add_column("Paths", style="white")
|
|
186
|
+
|
|
187
|
+
for file_id, paths in duplicates.items():
|
|
188
|
+
table.add_row(file_id, str(len(paths)), "\n".join(str(p) for p in paths))
|
|
189
|
+
|
|
190
|
+
console.print(table)
|
|
191
|
+
|
|
192
|
+
console.print(
|
|
193
|
+
Panel(
|
|
194
|
+
f"Found {len(duplicates)} sets of duplicate files\n"
|
|
195
|
+
f"Total duplicate files: {sum(len(paths) for paths in duplicates.values())}",
|
|
196
|
+
title="Duplicate Summary",
|
|
197
|
+
border_style="yellow",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
console.print(
|
|
202
|
+
Panel(
|
|
203
|
+
"[green]✓[/green] No duplicate files found",
|
|
204
|
+
title="No Duplicates",
|
|
205
|
+
border_style="green",
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.command()
|
|
211
|
+
def demo(
|
|
212
|
+
verbose: bool = typer.Option(
|
|
213
|
+
False, "--verbose", "-v", help="Enable verbose output"
|
|
214
|
+
),
|
|
215
|
+
):
|
|
216
|
+
"""
|
|
217
|
+
🎯 Demonstrate file tracking capabilities.
|
|
218
|
+
|
|
219
|
+
Creates test files and demonstrates how OS-level identifiers
|
|
220
|
+
persist through moves but change with copies.
|
|
221
|
+
"""
|
|
222
|
+
config = Config()
|
|
223
|
+
tracker = FileTracker(verbose=verbose)
|
|
224
|
+
|
|
225
|
+
console.print(
|
|
226
|
+
Panel(
|
|
227
|
+
"[blue]Creating demonstration files...[/blue]",
|
|
228
|
+
title="File Tracking Demo",
|
|
229
|
+
border_style="blue",
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Create test files
|
|
234
|
+
test_dir = Path("file_tracking_demo")
|
|
235
|
+
test_dir.mkdir(exist_ok=True)
|
|
236
|
+
|
|
237
|
+
original_file = test_dir / "original.txt"
|
|
238
|
+
original_file.write_text("This is the original file content.")
|
|
239
|
+
|
|
240
|
+
# Get original identifier
|
|
241
|
+
original_id = tracker.get_file_identifier(original_file)
|
|
242
|
+
original_metadata = tracker.get_file_metadata(original_file)
|
|
243
|
+
|
|
244
|
+
console.print(f"[green]Original file created:[/green] {original_file}")
|
|
245
|
+
console.print(f"[green]Original identifier:[/green] {original_id}")
|
|
246
|
+
|
|
247
|
+
# Copy the file (creates new inode)
|
|
248
|
+
copied_file = test_dir / "copied.txt"
|
|
249
|
+
import shutil
|
|
250
|
+
|
|
251
|
+
shutil.copy2(original_file, copied_file)
|
|
252
|
+
copied_id = tracker.get_file_identifier(copied_file)
|
|
253
|
+
|
|
254
|
+
console.print(f"[yellow]Copied file created:[/yellow] {copied_file}")
|
|
255
|
+
console.print(f"[yellow]Copied identifier:[/yellow] {copied_id}")
|
|
256
|
+
console.print(f"[yellow]Same identifier?[/yellow] {original_id == copied_id}")
|
|
257
|
+
|
|
258
|
+
# Move the original file (preserves inode)
|
|
259
|
+
moved_file = test_dir / "moved.txt"
|
|
260
|
+
shutil.move(str(original_file), str(moved_file))
|
|
261
|
+
moved_id = tracker.get_file_identifier(moved_file)
|
|
262
|
+
|
|
263
|
+
console.print(f"[blue]Moved file:[/blue] {moved_file}")
|
|
264
|
+
console.print(f"[blue]Moved identifier:[/blue] {moved_id}")
|
|
265
|
+
console.print(f"[blue]Same as original?[/blue] {original_id == moved_id}")
|
|
266
|
+
|
|
267
|
+
# Create summary table
|
|
268
|
+
table = Table(title="File Tracking Demonstration Results")
|
|
269
|
+
table.add_column("Operation", style="cyan")
|
|
270
|
+
table.add_column("File", style="green")
|
|
271
|
+
table.add_column("Identifier", style="magenta")
|
|
272
|
+
table.add_column("Same as Original?", style="yellow")
|
|
273
|
+
|
|
274
|
+
table.add_row("Original", str(original_file), str(original_id), "N/A")
|
|
275
|
+
table.add_row(
|
|
276
|
+
"Copy", str(copied_file), str(copied_id), str(original_id == copied_id)
|
|
277
|
+
)
|
|
278
|
+
table.add_row("Move", str(moved_file), str(moved_id), str(original_id == moved_id))
|
|
279
|
+
|
|
280
|
+
console.print(table)
|
|
281
|
+
|
|
282
|
+
# Cleanup
|
|
283
|
+
console.print(f"[red]Cleaning up demo files...[/red]")
|
|
284
|
+
import shutil
|
|
285
|
+
|
|
286
|
+
shutil.rmtree(test_dir)
|
|
287
|
+
|
|
288
|
+
console.print(
|
|
289
|
+
Panel(
|
|
290
|
+
"[green]✓[/green] Demonstration completed!\n\n"
|
|
291
|
+
"[bold]Key Insights:[/bold]\n"
|
|
292
|
+
"• Moving files preserves the OS identifier\n"
|
|
293
|
+
"• Copying files creates a new identifier\n"
|
|
294
|
+
"• This allows tracking files even when moved\n"
|
|
295
|
+
"• Database can store device:inode for persistent tracking",
|
|
296
|
+
title="Demo Complete",
|
|
297
|
+
border_style="green",
|
|
298
|
+
)
|
|
299
|
+
)
|