mirror of
https://github.com/sreedevk/deduplicator.git
synced 2026-08-26 18:15:33 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12295d7847 | ||
|
|
d81f499db3 | ||
|
|
9e1360aeb4 | ||
|
|
38ea37711f | ||
|
|
dbf504fc17 | ||
|
|
06efb2ed6f | ||
|
|
95af6c4a70 | ||
|
|
dd4b051378 | ||
|
|
533f81f724 | ||
|
|
471e60fa6c | ||
|
|
3fd4869869 | ||
|
|
99b87cf7fa | ||
|
|
b826dbe118 | ||
|
|
0f33b4d6b5 |
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[Bug] Title"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
** Runtime Info **
|
||||
App Arguments: [e.g. `-i --nocache`]
|
||||
Install Type: [e.g. `cargo install`]
|
||||
App Version: [e.g. v0.0.7]
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Platform Details (please complete the following information):**
|
||||
- OS: [e.g. Arch Linux]
|
||||
- Terminal Emulator: [e.g Alacritty]
|
||||
- Shell [e.g. Zshell]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature] Title"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
27
CONTRIBUTING.md
Normal file
27
CONTRIBUTING.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## How to contribute to Deduplicator
|
||||
|
||||
#### **Did you find a bug?**
|
||||
|
||||
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/sreedevk/deduplicator/issues).
|
||||
|
||||
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/sreedevk/deduplicator/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
|
||||
|
||||
* If possible, use the [bug report template](https://github.com/sreedevk/deduplicator/blob/main/.github/ISSUE_TEMPLATE/bug_report.md) to create the issue.
|
||||
|
||||
#### **Would you like to write a fix for the bug?**
|
||||
* Assign the Issue to yourself (if unassigned) before you start working in order to avoid any conficts.
|
||||
* Open a new GitHub pull request with the patch.
|
||||
* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number.
|
||||
|
||||
#### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
|
||||
|
||||
Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Deduplicator will generally not be accepted/
|
||||
|
||||
#### **Do you intend to add a new feature or change an existing one?**
|
||||
|
||||
* First open an issue with the sugggestion using the [feature request template](https://github.com/sreedevk/deduplicator/blob/main/.github/ISSUE_TEMPLATE/feature-request.md)
|
||||
* Do not create a PR before one of the core contributors has conveyed acceptance for a feature request.
|
||||
|
||||
#### **Do you have questions about the source code?**
|
||||
|
||||
* If you have a question, raise an issue in the repository with a "question" label.
|
||||
50
Cargo.lock
generated
50
Cargo.lock
generated
@@ -161,6 +161,19 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9b6515d269224923b26b5febea2ed42b2d5f2ce37284a4dd670fedd6cb8347a"
|
||||
dependencies = [
|
||||
"encode_unicode 0.3.6",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
@@ -303,7 +316,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deduplicator"
|
||||
version = "0.0.6"
|
||||
version = "0.0.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -313,6 +326,7 @@ dependencies = [
|
||||
"fxhash",
|
||||
"glob",
|
||||
"humansize",
|
||||
"indicatif",
|
||||
"itertools",
|
||||
"prettytable-rs",
|
||||
"rayon",
|
||||
@@ -349,6 +363,12 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
@@ -459,6 +479,20 @@ dependencies = [
|
||||
"cxx-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4295cbb7573c16d310e99e713cf9e75101eb190ab31fccd35f2d2691b4352b19"
|
||||
dependencies = [
|
||||
"console",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"rayon",
|
||||
"tokio",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.3"
|
||||
@@ -613,6 +647,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.16.0"
|
||||
@@ -660,6 +700,12 @@ version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b"
|
||||
|
||||
[[package]]
|
||||
name = "prettytable-rs"
|
||||
version = "0.10.0"
|
||||
@@ -667,7 +713,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
|
||||
dependencies = [
|
||||
"csv",
|
||||
"encode_unicode",
|
||||
"encode_unicode 1.0.0",
|
||||
"is-terminal",
|
||||
"lazy_static",
|
||||
"term",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deduplicator"
|
||||
version = "0.0.6"
|
||||
version = "0.0.8"
|
||||
edition = "2021"
|
||||
description = "find,filter,delete Duplicates"
|
||||
license = "MIT"
|
||||
@@ -17,6 +17,7 @@ crossterm = "0.25.0"
|
||||
fxhash = "0.2.1"
|
||||
glob = "0.3.0"
|
||||
humansize = "2.1.2"
|
||||
indicatif = { version = "0.17.2", features = ["rayon", "tokio"] }
|
||||
itertools = "0.10.5"
|
||||
prettytable-rs = "0.10.0"
|
||||
rayon = "1.6.1"
|
||||
|
||||
12
README.md
12
README.md
@@ -16,7 +16,8 @@ Usage: deduplicator [OPTIONS]
|
||||
Options:
|
||||
-t, --types <TYPES> Filetypes to deduplicate (default = all)
|
||||
--dir <DIR> Run Deduplicator on dir different from pwd
|
||||
-n, --nocache Don't use cache for indexing files (default = true)
|
||||
-n, --nocache Don't use cache for indexing files (default = false)
|
||||
-i, --interactive Delete files interactively
|
||||
-h, --help Print help information
|
||||
-V, --version Print version information
|
||||
```
|
||||
@@ -42,4 +43,11 @@ cargo install deduplicator
|
||||
</p>
|
||||
<h2 align="center">Screenshots</h2>
|
||||
|
||||

|
||||
<p align="center>
|
||||
<a href="https://asciinema.org/a/oMzGUSlAx32xYXcPBY64uAuLB" target="_blank"><img src="https://asciinema.org/a/oMzGUSlAx32xYXcPBY64uAuLB.svg" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img align="center" src="https://user-images.githubusercontent.com/36154121/211458077-90092aa3-496c-492f-a061-618059890d5f.png" width="500" height="400" />
|
||||
</p>
|
||||
|
||||
|
||||
14
src/app/file_manager.rs
Normal file
14
src/app/file_manager.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use crate::database::File;
|
||||
use anyhow::Result;
|
||||
use colored::Colorize;
|
||||
|
||||
pub fn delete_files(files: Vec<File>) -> Result<()> {
|
||||
files.into_iter().for_each(|file| {
|
||||
match std::fs::remove_file(file.path.clone()) {
|
||||
Ok(_) => println!("{}: {}", "DELETED".green(), file.path),
|
||||
Err(e) => println!("{}: {}", "FAILED".red(), file.path)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
#![allow(unused)]
|
||||
|
||||
mod event_handler;
|
||||
mod events;
|
||||
mod formatter;
|
||||
mod ui;
|
||||
pub mod file_manager;
|
||||
|
||||
use std::{io, thread, time::Duration};
|
||||
|
||||
@@ -32,7 +35,11 @@ impl App {
|
||||
// Self::init_render_loop(&mut term)?;
|
||||
// Self::cleanup(&mut term)?;
|
||||
|
||||
output::print(duplicates, app_args); /* TODO: APP TUI INIT FUNCTION */
|
||||
match app_args.interactive {
|
||||
true => output::interactive(duplicates, app_args),
|
||||
false => output::print(duplicates, app_args) /* TODO: APP TUI INIT FUNCTION */
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use std::{collections::HashMap, fs};
|
||||
use std::{collections::HashMap, fs, io};
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::offset::Utc;
|
||||
use chrono::DateTime;
|
||||
use colored::Colorize;
|
||||
use humansize::{format_size, DECIMAL};
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::app::file_manager;
|
||||
use crate::database::File;
|
||||
use crate::params::Params;
|
||||
use prettytable::{row, Cell, Row, format, Table};
|
||||
use prettytable::{format, row, Cell, Row, Table};
|
||||
|
||||
fn format_path(path: &str, opts: &Params) -> Result<String> {
|
||||
let display_path = path.replace(&opts.get_directory()?, "");
|
||||
@@ -50,16 +53,104 @@ fn group_duplicates(duplicates: Vec<File>) -> HashMap<String, Vec<File>> {
|
||||
duplicate_mapper
|
||||
}
|
||||
|
||||
fn print_meta_info(duplicates: &Vec<File>, opts: &Params) {
|
||||
println!("Deduplicator v{}", std::env!("CARGO_PKG_VERSION"));
|
||||
}
|
||||
|
||||
fn scan_group_instruction() -> Result<String> {
|
||||
println!("\nEnter the indices of the files you want to delete.");
|
||||
println!("You can enter multiple files using commas to seperate file indices.");
|
||||
println!("example: 1,2");
|
||||
print!("\n> ");
|
||||
std::io::stdout().flush()?;
|
||||
let mut user_input = String::new();
|
||||
io::stdin().read_line(&mut user_input)?;
|
||||
|
||||
Ok(user_input)
|
||||
}
|
||||
|
||||
fn scan_group_confirmation() -> Result<bool> {
|
||||
print!("\nconfirm? [Y/n]: ");
|
||||
std::io::stdout().flush()?;
|
||||
let mut user_input = String::new();
|
||||
io::stdin().read_line(&mut user_input)?;
|
||||
|
||||
match user_input.trim() {
|
||||
"Y" | "y" => Ok(true),
|
||||
_ => Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn process_group_action(duplicates: &Vec<File>, dup_index: usize, dup_size: usize, table: Table) {
|
||||
println!("\nDuplicate Set {} of {}\n", dup_index + 1, dup_size);
|
||||
table.printstd();
|
||||
let files_to_delete = scan_group_instruction().unwrap_or_default();
|
||||
let parsed_file_indices = files_to_delete
|
||||
.trim()
|
||||
.split(',')
|
||||
.filter(|element| !element.is_empty())
|
||||
.map(|index| index.parse::<usize>().unwrap_or_default())
|
||||
.collect_vec();
|
||||
|
||||
if parsed_file_indices
|
||||
.clone()
|
||||
.into_iter()
|
||||
.any(|index| index > (duplicates.len() - 1))
|
||||
{
|
||||
println!("{}", "Err: File Index Out of Bounds!".red());
|
||||
return process_group_action(duplicates, dup_index, dup_size, table);
|
||||
}
|
||||
|
||||
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
|
||||
|
||||
if parsed_file_indices.is_empty() { return }
|
||||
|
||||
let files_to_delete = parsed_file_indices
|
||||
.into_iter()
|
||||
.map(|index| duplicates[index].clone());
|
||||
|
||||
println!("\n{}", "The following files will be deleted:".red());
|
||||
files_to_delete.clone().enumerate().for_each(|(index, file)| {
|
||||
println!("{}: {}", index.to_string().blue(), file.path);
|
||||
});
|
||||
|
||||
match scan_group_confirmation().unwrap() {
|
||||
true => { file_manager::delete_files(files_to_delete.collect_vec()); },
|
||||
false => println!("{}", "\nCancelled Delete Operation.".red())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interactive(duplicates: Vec<File>, opts: &Params) {
|
||||
print_meta_info(&duplicates, opts);
|
||||
let grouped_duplicates = group_duplicates(duplicates);
|
||||
|
||||
grouped_duplicates.iter().enumerate().for_each(|(gindex, (hash, group))| {
|
||||
let mut itable = Table::new();
|
||||
itable.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
itable.set_titles(row!["index", "filename", "size", "updated_at"]);
|
||||
group.iter().enumerate().for_each(|(index, file)| {
|
||||
itable.add_row(row![
|
||||
index,
|
||||
format_path(&file.path, opts).unwrap_or_default().blue(),
|
||||
file_size(&file.path).unwrap_or_default().red(),
|
||||
modified_time(&file.path).unwrap_or_default().yellow()
|
||||
]);
|
||||
});
|
||||
|
||||
process_group_action(group, gindex, grouped_duplicates.len(), itable);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn print(duplicates: Vec<File>, opts: &Params) {
|
||||
print_meta_info(&duplicates, opts);
|
||||
|
||||
let mut output_table = Table::new();
|
||||
let grouped_duplicates: HashMap<String, Vec<File>> = group_duplicates(duplicates);
|
||||
|
||||
output_table.set_titles(row!["hash", "duplicates"]);
|
||||
grouped_duplicates.iter().for_each(|(hash, group)| {
|
||||
let mut inner_table = Table::new();
|
||||
// inner_table.set_format(inner_table_format);
|
||||
inner_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
//inner_table.set_titles(row!["filename", "size", "updated_at"]);
|
||||
group.iter().for_each(|file| {
|
||||
inner_table.add_row(row![
|
||||
format_path(&file.path, opts).unwrap_or_default().blue(),
|
||||
|
||||
@@ -12,9 +12,12 @@ pub struct Params {
|
||||
/// Run Deduplicator on dir different from pwd
|
||||
#[arg(long)]
|
||||
pub dir: Option<PathBuf>,
|
||||
/// Don't use cache for indexing files (default = true)
|
||||
/// Don't use cache for indexing files (default = false)
|
||||
#[arg(long, short)]
|
||||
pub nocache: bool,
|
||||
/// Delete files interactively
|
||||
#[arg(long, short)]
|
||||
pub interactive: bool
|
||||
}
|
||||
|
||||
impl Params {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle, ParallelProgressIterator};
|
||||
use anyhow::Result;
|
||||
use fxhash::hash32 as hasher;
|
||||
use glob::glob;
|
||||
@@ -46,6 +46,7 @@ fn scan(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<String
|
||||
let indexed_paths = database::indexed_paths(connection)?;
|
||||
let files: Vec<String> = glob_patterns
|
||||
.par_iter()
|
||||
.progress_with_style(ProgressStyle::with_template("{spinner:.green} [scanning files] [{wide_bar:.cyan/blue}] {pos}/{len} files").unwrap())
|
||||
.filter_map(|glob_pattern| glob(glob_pattern.as_os_str().to_str()?).ok())
|
||||
.flat_map(|file_vec| {
|
||||
file_vec
|
||||
@@ -66,6 +67,7 @@ fn scan(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<String
|
||||
fn index_files(files: Vec<String>, connection: &sqlite::Connection) -> Result<()> {
|
||||
let hashed: Vec<File> = files
|
||||
.into_par_iter()
|
||||
.progress_with_style(ProgressStyle::with_template("{spinner:.green} [indexing files] [{wide_bar:.cyan/blue}] {pos}/{len} files").unwrap())
|
||||
.filter_map(|file| {
|
||||
let hash = hash_file(&file).ok()?;
|
||||
Some(database::File { path: file, hash })
|
||||
|
||||
Reference in New Issue
Block a user