diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc48c68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.vs + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..dcc4d80 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: rust +rust: + - stable + - beta + - nightly +cache: cargo +jobs: + allow_failures: + - rust: nightly + fast_finish: true +addons: + apt: + packages: + - libgtk-3-dev + update: true +os: + - linux + - osx + - windows +script: + - cargo build --verbose --release --features gui \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fbc71ca --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +#![windows_subsystem = "windows"] + +[package] +name = "blackhole" +version = "1.0.0" +authors = ["William Venner "] +edition = "2018" +repository = "https://github.com/WilliamVenner/blackhole" +license = "MIT" +keywords = ["blackhole", "black hole", "forget", "ramdisk", "organization", "organisation", "organisation-tool", "organization-tool", "black-hole"] + +[features] +gui = ["msgbox"] + +[dependencies] +dirs = "3.0" +trash = "1.2" +msgbox = { version = "0.6.0", optional = true } + +[target.'cfg(windows)'.dependencies] +powershell_script = "0.1" +winapi = { version = "0.3", features = ["winuser"] } +rust-ini = "0.16" +winreg = "0.8" + +[target.'cfg(windows)'.build-dependencies] +winres = "0.1" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cab760 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +

+ Logo +

+ +## Blackhole + +[![Travis CI Build Status](https://travis-ci.com/WilliamVenner/blackhole.svg?token=GXuyFsyVxqMmbV5zG6B4&branch=master)](https://travis-ci.com/github/WilliamVenner/blackhole) + +Blackhole is a simple program that creates a folder in your computer's home directory where **files may not return**. + +Every time you start your computer/log into your user account, the Blackhole directory is moved to your computer's Recycle Bin/Trash, where you can restore it if needed. + +## Releases + +[Click here](https://github.com/WilliamVenner/blackhole/releases) for pre-built binaries. + +## Requirements + +* Your operating system must have some form of "Recycle Bin" or "Trash" +* Your operating system must provide you a home directory +* The program may require administrative/elevated privileges on some operating systems + +## Windows + +Blackhole will automatically add itself to your startup programs via the registry. + +If contents are present, the `$BLACKHOLE` directory will be moved to the Recycle Bin every time you start up your computer. + +The `$BLACKHOLE` directory will automatically be added to the Quick Access locations. + +#### File Location + +`%USERPROFILE%/$BLACKHOLE` + +## Linux & MacOS + +Purging the Blackhole at startup is not yet supported on these operating systems. + +If you know Rust and a bit about your favourite OS, pull requests are appreciated. + +#### File Location + +`$HOME/$BLACKHOLE` + +## Screenshots + +

+ Windows +

+ +## Credits + +Icon made by [Flat Icons](https://www.flaticon.com/authors/flat-icons) from [www.flaticon.com](https://www.flaticon.com) diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b338701 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +use std::io; +#[cfg(windows)] use winres::WindowsResource; + +fn main() -> io::Result<()> { + #[cfg(windows)] { + WindowsResource::new().set_icon("src/assets/blackhole.ico").compile()?; + } + Ok(()) +} \ No newline at end of file diff --git a/src/assets/blackhole.ico b/src/assets/blackhole.ico new file mode 100644 index 0000000..ef918a0 Binary files /dev/null and b/src/assets/blackhole.ico differ diff --git a/src/blackhole.rs b/src/blackhole.rs new file mode 100644 index 0000000..2d3c76e --- /dev/null +++ b/src/blackhole.rs @@ -0,0 +1,88 @@ +pub mod blackhole { + use crate::Show; + + use std::{ffi::OsString, fs, io}; + use dirs; + use trash; + + pub struct Blackhole { + pub path: std::path::PathBuf + } + + impl Blackhole { + pub fn new(should_purge: bool) -> Result { + let mut home_dir = match dirs::home_dir() { + Some(home_dir) => home_dir, + None => { return Err("Could not find a home directory!") }, + }; + + home_dir.push("$BLACKHOLE"); + + let new = Blackhole { path: home_dir }; + + // If the blackhole directory doesn't exist, create it + new.create(); + println!("Blackhole initialized!"); + + // If it does exist & we've started up in purge mode, delete it + if should_purge { new.purge() } + + // Run any chores + #[cfg(any(windows, linux))] + new.chores(); + + Ok(new) + } + + fn create(&self) { + if self.path.is_dir() { return }; + + match fs::create_dir(&self.path) { + Err(error) => Show::panic(&format!("Failed to CREATE blackhole directory (\"{:?}\") at {:?}", error, self.path)), + Ok(_) => return + } + } + + fn empty(&self) -> Result { + if !cfg!(windows) { return Ok(self.path.read_dir()?.next().is_none()) } + + let desktop_ini: OsString = OsString::from("desktop.ini"); + for entry in fs::read_dir(&self.path)? { + match entry?.path().file_name() { + None => continue, + Some(file_name) => { + if file_name != desktop_ini { + return Ok(false); + } + } + } + } + + Ok(true) + } + + fn purge(&self) { + match self.empty() { + Err(error) => Show::panic(&format!("Failed to READ blackhole directory (\"{:?}\") at {:?}", error, self.path)), + Ok(empty) => { + if empty { + println!("Blackhole directory empty."); + return + } + } + } + + match trash::delete(&self.path) { + Err(error) => Show::panic(&format!("Failed to PURGE blackhole directory (\"{:?}\") at {:?}", error, self.path)), + Ok(_) => () + } + + println!("Purged Blackhole directory!"); + + self.create(); + } + } + + #[cfg(windows)] use crate::windows::Windows; + #[cfg(linux)] use crate::linux::Linux; +} \ No newline at end of file diff --git a/src/linux.rs b/src/linux.rs new file mode 100644 index 0000000..67487bb --- /dev/null +++ b/src/linux.rs @@ -0,0 +1,49 @@ +use crate::Blackhole; +use crate::Show; + +use std::{env, fs}; + +pub trait Linux { + fn copy_executable(); + fn chores(&self); +} + +impl Linux for Blackhole { + fn copy_executable() { + // Locate the executable folder and symlink us there + let mut new_exe_path = match dirs::executable_dir() { + Ok(new_exe_path) => new_exe_path, + Err(error) => { + Show::panic("Error getting executable directory: {}", error); + return; + } + }; + + new_exe_path.push("blackhole"); + + let exe_path = match std::env::current_exe() { + Ok(exe_path) => exe_path, + Err(error) => { + Show::panic("Error getting executable path: {}", error); + return; + } + }; + + if exe_path == new_exe_path { return } + + match fs::rename() { + Ok(_) => return, + Err(error) => { + Show::panic("Error moving executable to executables path: {}", error); + return; + } + } + + Show::msg("Blackhole executable has been moved to {}", new_exe_path); + } + + fn chores(&self) { + // If we're running Linux, copy the executable to the executables folder + Blackhole::copy_executable(); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..744742b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,18 @@ +use std::env; + +mod show; +use show::show::Show; + +mod blackhole; +use blackhole::blackhole::Blackhole; + +#[cfg(windows)] mod windows; +#[cfg(linux)] mod linux; + +fn main() { + let should_purge: bool = env::args_os().any(|arg| arg == "--purge"); + match Blackhole::new(should_purge) { + Ok(blackhole) => { println!("Location: {}", blackhole.path.display()) }, + Err(error) => { Show::panic(&String::from(error)); } + } +} diff --git a/src/show.rs b/src/show.rs new file mode 100644 index 0000000..0947924 --- /dev/null +++ b/src/show.rs @@ -0,0 +1,41 @@ +pub mod show { + pub struct Show(); + + #[cfg(not(feature="gui"))] + impl Show { + pub fn panic(err: &String) { + panic!("Blackhole PANIC: {}", err); + } + + pub fn error(err: &String) { + println!("Blackhole ERROR: {}", err); + } + + pub fn msg(err: &String) { + println!("Blackhole INFO: {}", err); + } + } + + #[cfg(feature="gui")] extern crate msgbox; + #[cfg(feature="gui")] use msgbox::IconType; + #[cfg(feature="gui")] + impl Show { + pub fn panic(err: &String) { + if msgbox::create("Blackhole PANIC", err, IconType::Error).is_err() { + panic!("Blackhole PANIC: {}", err); + } + } + + pub fn error(err: &String) { + if msgbox::create("Blackhole ERROR", err, IconType::Error).is_err() { + println!("Blackhole ERROR: {}", err); + } + } + + pub fn msg(err: &String) { + if msgbox::create("Blackhole", err, IconType::Info).is_err() { + println!("Blackhole INFO: {}", err); + } + } + } +} \ No newline at end of file diff --git a/src/windows.rs b/src/windows.rs new file mode 100644 index 0000000..8a3f8ff --- /dev/null +++ b/src/windows.rs @@ -0,0 +1,132 @@ +use crate::Blackhole; +use crate::Show; + +use std::{iter, fs, os::windows::ffi::OsStrExt, process::Command}; +use ini::{Ini, EscapePolicy}; +use winreg::{enums::*, RegKey}; +use winapi; + +pub trait Windows { + fn powershell(script: String); + fn quick_access(path: &std::path::PathBuf); + fn set_file_attributes(path: &std::path::PathBuf, attr: winapi::shared::minwindef::DWORD); + fn set_blackhole_attributes(path: &std::path::PathBuf); + fn edit_startup_registry(); + fn chores(&self); +} + +impl Windows for Blackhole { + // Run a PowerShell script + // TODO: Replace these with winapi calls when the relevant functions are binded + fn powershell(script: String) { + Command::new("powershell").args(&["-Command", &script]).status().ok(); + } + + // Add $BLACKHOLE to the Quick Access tab + fn quick_access(path: &std::path::PathBuf) { + Blackhole::powershell(format!("$o = new-object -com shell.application\n$o.Namespace('{}').Self.InvokeVerb('pintohome')", path.display())); + } + + // Sets Windows file attributes + fn set_file_attributes(path: &std::path::PathBuf, attr: winapi::shared::minwindef::DWORD) { + let path_utf16: Vec = path.as_os_str().encode_wide().chain(iter::once(0)).collect(); + unsafe { + winapi::um::fileapi::SetFileAttributesW(path_utf16.as_ptr(), attr); + } + } + + // Configures the $BLACKHOLE folder + fn set_blackhole_attributes(path: &std::path::PathBuf) { + // Set the file attributes of the blackhole directory itself + Blackhole::set_file_attributes(path, winapi::um::winnt::FILE_ATTRIBUTE_SYSTEM | winapi::um::winnt::FILE_ATTRIBUTE_READONLY); + + // Create desktop.ini + let mut ini = path.to_owned(); + ini.push("desktop.ini"); + + if ini.exists() { + match fs::remove_file(&ini) { + Ok(_) => (), + Err(error) => { + println!("Error deleting desktop.ini: {}", error); + } + }; + } + + let mut desktop = Ini::new(); + desktop.with_section(Some(".ShellClassInfo")) + .set("ConfirmFileOp", "0") + .set("InfoTip", "WARNING: All files stored here will be deleted at the next startup"); + + desktop.with_section(Some("ViewState")) + .set("Mode", "") + .set("Vid", "") + .set("FolderType", "Generic"); + + // Copy the icon from the exe over to the blackhole directory + match std::env::current_exe() { + Ok(exe_path) => { + match exe_path.into_os_string().into_string() { + Ok(exe_path) => { desktop.with_section(Some(".ShellClassInfo")).set("IconResource", format!("{},0", exe_path)); } + Err(_) => { + println!("Error converting executable path string"); + return; + } + } + }, + Err(error) => { + println!("Error getting executable path: {}", error); + return; + } + } + + // Write desktop.ini and set its file attributes + let escape_policy : ini::EscapePolicy = EscapePolicy::Nothing; + match desktop.write_to_file_policy(&ini, escape_policy) { + Ok(_) => { Blackhole::set_file_attributes(&ini, winapi::um::winnt::FILE_ATTRIBUTE_HIDDEN | winapi::um::winnt::FILE_ATTRIBUTE_SYSTEM); } + Err(error) => { + println!("Error setting file attributes on desktop.ini: {}", error); + return; + } + } + } + + fn edit_startup_registry() { + // Add blackhole.exe --purge to the startup registry + let exe_path = match std::env::current_exe() { + Ok(exe_path) => exe_path, + Err(error) => { + Show::error(&format!("Error getting executable path: {}", error)); + return; + } + }; + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let startup = match hkcu.open_subkey_with_flags("Software\\Microsoft\\Windows\\CurrentVersion\\Run", KEY_WRITE) { + Ok(startup) => startup, + Err(error) => { + Show::error(&format!("Error opening registry subkey: {}", error)); + return; + } + }; + + match startup.set_value("Blackhole", &format!("{} --purge", exe_path.display())) { + Ok(_) => return, + Err(error) => { + Show::error(&format!("Error setting registry key: {}", error)); + return; + } + }; + } + + fn chores(&self) { + // If we're running Windows, add blackhole.exe --purge to the startup registry + Blackhole::edit_startup_registry(); + + // Set file/folder attributes + Blackhole::set_blackhole_attributes(&self.path); + + // Add $BLACKHOLE to Quick Access links + Blackhole::quick_access(&self.path); + } +} \ No newline at end of file