dupe-hunter 0.1.0__tar.gz
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.
- dupe_hunter-0.1.0/PKG-INFO +62 -0
- dupe_hunter-0.1.0/README.md +46 -0
- dupe_hunter-0.1.0/pyproject.toml +30 -0
- dupe_hunter-0.1.0/setup.cfg +4 -0
- dupe_hunter-0.1.0/setup.py +3 -0
- dupe_hunter-0.1.0/src/__init__.py +1 -0
- dupe_hunter-0.1.0/src/dupe_hunter.egg-info/PKG-INFO +62 -0
- dupe_hunter-0.1.0/src/dupe_hunter.egg-info/SOURCES.txt +11 -0
- dupe_hunter-0.1.0/src/dupe_hunter.egg-info/dependency_links.txt +1 -0
- dupe_hunter-0.1.0/src/dupe_hunter.egg-info/entry_points.txt +2 -0
- dupe_hunter-0.1.0/src/dupe_hunter.egg-info/requires.txt +3 -0
- dupe_hunter-0.1.0/src/dupe_hunter.egg-info/top_level.txt +2 -0
- dupe_hunter-0.1.0/src/dupe_hunter.py +168 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dupe-hunter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find and remove duplicate files quickly
|
|
5
|
+
Author-email: lalalaleishaaa <your-email@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lalalaleishaaa/dupe-hunter
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/lalalaleishaaa/dupe-hunter/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: typer>=0.9.0
|
|
14
|
+
Requires-Dist: rich>=13.0.0
|
|
15
|
+
Requires-Dist: click>=8.0.0
|
|
16
|
+
|
|
17
|
+
\# 🔍 Dupe Hunter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
\*\*Find and delete duplicate files in seconds!\*\*
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
\## Real Results
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
I ran this on my own computer and:
|
|
30
|
+
|
|
31
|
+
\- Found \*\*16,541 duplicate files\*\*
|
|
32
|
+
|
|
33
|
+
\- Saved \*\*3.1 GB\*\* of disk space
|
|
34
|
+
|
|
35
|
+
\- Scanned \*\*50,000+ files\*\* in minutes
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
\## Features
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
\- \*\*Blazing fast\*\* - Groups by file size first
|
|
44
|
+
|
|
45
|
+
\- \*\*Progress bars\*\* - See what's happening
|
|
46
|
+
|
|
47
|
+
\- \*\*Beautiful output\*\* - Color-coded tables
|
|
48
|
+
|
|
49
|
+
\- \*\*Space calculator\*\* - Shows how much you'll save
|
|
50
|
+
|
|
51
|
+
\- \*\*Safe delete\*\* - Keeps first file, deletes rest
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
\## Installation
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
|
|
61
|
+
pip install dupe-hunter
|
|
62
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
\# 🔍 Dupe Hunter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
\*\*Find and delete duplicate files in seconds!\*\*
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
\## Real Results
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
I ran this on my own computer and:
|
|
14
|
+
|
|
15
|
+
\- Found \*\*16,541 duplicate files\*\*
|
|
16
|
+
|
|
17
|
+
\- Saved \*\*3.1 GB\*\* of disk space
|
|
18
|
+
|
|
19
|
+
\- Scanned \*\*50,000+ files\*\* in minutes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
\## Features
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
\- \*\*Blazing fast\*\* - Groups by file size first
|
|
28
|
+
|
|
29
|
+
\- \*\*Progress bars\*\* - See what's happening
|
|
30
|
+
|
|
31
|
+
\- \*\*Beautiful output\*\* - Color-coded tables
|
|
32
|
+
|
|
33
|
+
\- \*\*Space calculator\*\* - Shows how much you'll save
|
|
34
|
+
|
|
35
|
+
\- \*\*Safe delete\*\* - Keeps first file, deletes rest
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
\## Installation
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
|
|
45
|
+
pip install dupe-hunter
|
|
46
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dupe-hunter"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Find and remove duplicate files quickly"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "lalalaleishaaa", email = "your-email@example.com"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"typer>=0.9.0",
|
|
21
|
+
"rich>=13.0.0",
|
|
22
|
+
"click>=8.0.0"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/lalalaleishaaa/dupe-hunter"
|
|
27
|
+
"Bug Tracker" = "https://github.com/lalalaleishaaa/dupe-hunter/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
dupe-hunter = "src.dupe_hunter:app"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This makes 'src' a proper Python package
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dupe-hunter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find and remove duplicate files quickly
|
|
5
|
+
Author-email: lalalaleishaaa <your-email@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lalalaleishaaa/dupe-hunter
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/lalalaleishaaa/dupe-hunter/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: typer>=0.9.0
|
|
14
|
+
Requires-Dist: rich>=13.0.0
|
|
15
|
+
Requires-Dist: click>=8.0.0
|
|
16
|
+
|
|
17
|
+
\# 🔍 Dupe Hunter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
\*\*Find and delete duplicate files in seconds!\*\*
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
\## Real Results
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
I ran this on my own computer and:
|
|
30
|
+
|
|
31
|
+
\- Found \*\*16,541 duplicate files\*\*
|
|
32
|
+
|
|
33
|
+
\- Saved \*\*3.1 GB\*\* of disk space
|
|
34
|
+
|
|
35
|
+
\- Scanned \*\*50,000+ files\*\* in minutes
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
\## Features
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
\- \*\*Blazing fast\*\* - Groups by file size first
|
|
44
|
+
|
|
45
|
+
\- \*\*Progress bars\*\* - See what's happening
|
|
46
|
+
|
|
47
|
+
\- \*\*Beautiful output\*\* - Color-coded tables
|
|
48
|
+
|
|
49
|
+
\- \*\*Space calculator\*\* - Shows how much you'll save
|
|
50
|
+
|
|
51
|
+
\- \*\*Safe delete\*\* - Keeps first file, deletes rest
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
\## Installation
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
|
|
61
|
+
pip install dupe-hunter
|
|
62
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/__init__.py
|
|
5
|
+
src/dupe_hunter.py
|
|
6
|
+
src/dupe_hunter.egg-info/PKG-INFO
|
|
7
|
+
src/dupe_hunter.egg-info/SOURCES.txt
|
|
8
|
+
src/dupe_hunter.egg-info/dependency_links.txt
|
|
9
|
+
src/dupe_hunter.egg-info/entry_points.txt
|
|
10
|
+
src/dupe_hunter.egg-info/requires.txt
|
|
11
|
+
src/dupe_hunter.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import hashlib
|
|
4
|
+
from rich.progress import track
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
# Fix for when running as installed package
|
|
10
|
+
import sys
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
def get_file_hash(filepath: Path):
|
|
17
|
+
hasher = hashlib.md5()
|
|
18
|
+
try:
|
|
19
|
+
with open(filepath, 'rb') as f:
|
|
20
|
+
for chunk in iter(lambda: f.read(8192), b''):
|
|
21
|
+
hasher.update(chunk)
|
|
22
|
+
return hasher.hexdigest()
|
|
23
|
+
except (PermissionError, OSError):
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
@app.command()
|
|
27
|
+
def scan(
|
|
28
|
+
folder: str = typer.Argument(".", help="Folder to scan for duplicates"),
|
|
29
|
+
delete: bool = typer.Option(False, "--delete", help="Delete duplicate files (keeps the first one)")
|
|
30
|
+
):
|
|
31
|
+
"""Find duplicate files in a folder."""
|
|
32
|
+
console.print(f"[bold blue]📂 Scanning:[/bold blue] {folder}")
|
|
33
|
+
|
|
34
|
+
folder_path = Path(folder)
|
|
35
|
+
if not folder_path.exists():
|
|
36
|
+
console.print(f"[red]❌ Error:[/red] Folder '{folder}' doesn't exist!")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
all_files = [f for f in folder_path.rglob("*") if f.is_file()]
|
|
40
|
+
|
|
41
|
+
if not all_files:
|
|
42
|
+
console.print("[yellow]⚠️ No files found![/yellow]")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
console.print(f"Found [green]{len(all_files)}[/green] files")
|
|
46
|
+
|
|
47
|
+
size_groups = defaultdict(list)
|
|
48
|
+
for file in track(all_files, description="Grouping by size..."):
|
|
49
|
+
size_groups[file.stat().st_size].append(file)
|
|
50
|
+
|
|
51
|
+
potential_dupes = {
|
|
52
|
+
size: files
|
|
53
|
+
for size, files in size_groups.items()
|
|
54
|
+
if len(files) > 1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if not potential_dupes:
|
|
58
|
+
console.print("[green]✅ No duplicates found![/green]")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
console.print(f"Found [yellow]{len(potential_dupes)}[/yellow] size groups")
|
|
62
|
+
|
|
63
|
+
duplicates = defaultdict(list)
|
|
64
|
+
for size, files in track(potential_dupes.items(), description="Hashing..."):
|
|
65
|
+
hash_groups = defaultdict(list)
|
|
66
|
+
for file in files:
|
|
67
|
+
file_hash = get_file_hash(file)
|
|
68
|
+
if file_hash:
|
|
69
|
+
hash_groups[file_hash].append(file)
|
|
70
|
+
|
|
71
|
+
for file_hash, dup_files in hash_groups.items():
|
|
72
|
+
if len(dup_files) > 1:
|
|
73
|
+
duplicates[file_hash] = dup_files
|
|
74
|
+
|
|
75
|
+
if not duplicates:
|
|
76
|
+
console.print("[green]✅ No exact duplicates found![/green]")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
total_dupes = sum(len(files) for files in duplicates.values())
|
|
80
|
+
console.print(f"\n[bold red]🔍 Found {total_dupes} duplicates in {len(duplicates)} groups:\n")
|
|
81
|
+
|
|
82
|
+
for file_hash, files in duplicates.items():
|
|
83
|
+
table = Table(title=f"Hash: {file_hash[:8]}...")
|
|
84
|
+
table.add_column("File", style="cyan")
|
|
85
|
+
table.add_column("Size", style="green")
|
|
86
|
+
|
|
87
|
+
for f in files:
|
|
88
|
+
size_kb = f.stat().st_size / 1024
|
|
89
|
+
if size_kb > 1024:
|
|
90
|
+
size_mb = size_kb / 1024
|
|
91
|
+
table.add_row(str(f), f"{size_mb:.2f} MB")
|
|
92
|
+
else:
|
|
93
|
+
table.add_row(str(f), f"{size_kb:.2f} KB")
|
|
94
|
+
|
|
95
|
+
console.print(table)
|
|
96
|
+
console.print("─" * 50)
|
|
97
|
+
|
|
98
|
+
# ==============================================
|
|
99
|
+
# NEW: Calculate and display space savings
|
|
100
|
+
# ==============================================
|
|
101
|
+
|
|
102
|
+
# Calculate total space wasted
|
|
103
|
+
total_size = 0
|
|
104
|
+
total_savings = 0
|
|
105
|
+
|
|
106
|
+
for file_hash, files in duplicates.items():
|
|
107
|
+
# Size of all duplicate files in this group
|
|
108
|
+
group_size = sum(f.stat().st_size for f in files)
|
|
109
|
+
total_size += group_size
|
|
110
|
+
|
|
111
|
+
# Keep one copy, delete the rest
|
|
112
|
+
# So savings = group_size - size_of_one_file
|
|
113
|
+
group_savings = group_size - files[0].stat().st_size
|
|
114
|
+
total_savings += group_savings
|
|
115
|
+
|
|
116
|
+
# Display the savings
|
|
117
|
+
if total_savings > 0:
|
|
118
|
+
console.print("")
|
|
119
|
+
if total_savings > 1024 * 1024: # More than 1 MB
|
|
120
|
+
console.print(f"[bold yellow]💾 Potential space saved: {total_savings / (1024 * 1024):.2f} MB[/bold yellow]")
|
|
121
|
+
elif total_savings > 1024: # More than 1 KB
|
|
122
|
+
console.print(f"[bold yellow]💾 Potential space saved: {total_savings / 1024:.2f} KB[/bold yellow]")
|
|
123
|
+
else:
|
|
124
|
+
console.print(f"[bold yellow]💾 Potential space saved: {total_savings} bytes[/bold yellow]")
|
|
125
|
+
|
|
126
|
+
console.print(f"[bold cyan]📊 SUMMARY[/bold cyan]")
|
|
127
|
+
console.print(f" Total duplicate groups: [yellow]{len(duplicates)}[/yellow]")
|
|
128
|
+
console.print(f" Total duplicate files: [yellow]{total_dupes}[/yellow]")
|
|
129
|
+
|
|
130
|
+
if total_savings > 1024 * 1024:
|
|
131
|
+
console.print(f" Total space wasted: [yellow]{total_savings / (1024 * 1024):.2f} MB[/yellow]")
|
|
132
|
+
elif total_savings > 1024:
|
|
133
|
+
console.print(f" Total space wasted: [yellow]{total_savings / 1024:.2f} KB[/yellow]")
|
|
134
|
+
else:
|
|
135
|
+
console.print(f" Total space wasted: [yellow]{total_savings} bytes[/yellow]")
|
|
136
|
+
|
|
137
|
+
# ==============================================
|
|
138
|
+
# END OF NEW CODE
|
|
139
|
+
# ==============================================
|
|
140
|
+
|
|
141
|
+
# If delete flag is used, remove duplicates
|
|
142
|
+
if delete:
|
|
143
|
+
console.print("\n[bold yellow]⚠️ DELETE MODE ACTIVATED[/bold yellow]")
|
|
144
|
+
console.print("[yellow]Keeping the first file in each group, deleting the rest...[/yellow]")
|
|
145
|
+
|
|
146
|
+
deleted_count = 0
|
|
147
|
+
for file_hash, files in duplicates.items():
|
|
148
|
+
# Keep first file, delete the rest
|
|
149
|
+
for file in files[1:]:
|
|
150
|
+
try:
|
|
151
|
+
file.unlink()
|
|
152
|
+
console.print(f"[red]🗑️ Deleted:[/red] {file}")
|
|
153
|
+
deleted_count += 1
|
|
154
|
+
except Exception as e:
|
|
155
|
+
console.print(f"[red]❌ Error:[/red] {e}")
|
|
156
|
+
|
|
157
|
+
if deleted_count > 0:
|
|
158
|
+
console.print(f"\n[bold green]✅ Deleted {deleted_count} duplicate files![/bold green]")
|
|
159
|
+
console.print(f"[bold green]💾 Saved {total_savings / (1024 * 1024):.2f} MB of space![/bold green]")
|
|
160
|
+
else:
|
|
161
|
+
console.print("\n[yellow]No files were deleted.[/yellow]")
|
|
162
|
+
|
|
163
|
+
@app.command()
|
|
164
|
+
def hello(name: str = "World"):
|
|
165
|
+
console.print(f"[bold green]Hello {name}! 🚀[/bold green]")
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
app()
|