diff --git a/.gitignore b/.gitignore index d2d6f36..b83d222 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1 @@ -*.py[cod] - -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox -nosetests.xml - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject +/target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..de7acef --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,144 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "csv" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "junction" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca39ef0d69b18e6a2fd14c2f0a1d593200f4a4ed949b240b5917ab51fac754cb" +dependencies = [ + "scopeguard", + "winapi", +] + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "save-central" +version = "2.0.0" +dependencies = [ + "csv", + "junction", + "winapi", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2c9eee4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "save-central" +version = "2.0.0" +edition = "2021" + +[dependencies] +junction = "=1.0" +winapi = "=0.3.9" +csv="1.1" diff --git a/LICENSE b/LICENSE index b8aad66..48cceb8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 James D +Copyright (c) 2023 JamesGecko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 7d35ef9..1be2985 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,27 @@ # Save Central -PC games put saves [all over the place](http://www.rockpapershotgun.com/2012/01/23/stop-it-put-save-games-in-one-place/). `%userprofile%/Saved Games` is the -official be all, end all location. Microsoft said so! You should just be able -to back up that folder and have a copy of all your saved games. But _noooo_. Some games clutter even up your `~/Documents` folder with their stupid junk. -It's super obnoxious and there's no reason for it. +PC games put saves [all over the place](http://www.rockpapershotgun.com/2012/01/23/stop-it-put-save-games-in-one-place/). -It's time to fight back. Save Central finds saved games, moves the saves to -`~/Saved Games`, and makes a hidden junction. Your games will still able to -find their saves, but they'll all be stored where they belong. +This utility moves game save files into `%userprofile%/Saved Games` and creates a hidden junction from the original location. Games will still work correctly, but they won't clutter up your Documents folder with save files. -As a side effect, it's slightly easier to back up your saves. - -### Requirements - -* Python 3.3 - -pip3 install -r requirements.txt - -### Moving and linking - -Just run the script! - -``` -python3 save-central.py -``` +This also makes it easier to back up saves for games that don't have cloud sync. ### Restoring backed up saves on a new computer -This is left as an exercise to the reader. Pull requests welcome! (If you're -absolutely up the creek without a paddle, just look at `list.csv` and manually -copy the folder to the correct location.) +Just run the utility again and save-central will create junctions for all the save files in `%userprofile%/Saved Games` -### FAQ +### Plausible questions #### It didn't move some folders? Edit `list.csv` to add the correct path. The first item on a line is the source, relative to `%userprofile%`. The second item is the destination, relative to -`%userprofile%/Saved Games`. Please send a pull request, too. Note the alphabetical order. - -#### What about games that use `~/Documents/My Games`? - -That is acceptable. I feel no ill will towards games which use what was the -recommended save game location a decade ago, when XP roamed the earth. -But times have changed. +`%userprofile%/Saved Games`. Pull requests welcome! Note the alphabetical order. -#### What about games that use Steam Cloud? +#### What about games that use `~/Documents/My Games`? Or cloud save? -What about them? Those saves are backed up automatically, so we only care about -them if they're living some place inappropriate in `~`. +Both are nice, but I still want a clean Documents directory. Into `%userprofile%/Saved Games` they go! #### What about games that don't put their save files anywhere in `%userprofile%`? -The only modern titles I've encountered that do this use UPlay. Not _all_ UPlay games. But at -least one. The paths for these games are in `unqualified_list.csv`. A lot of older games may -do this, also. My gaming library is such that I haven't encountered them. Pull requests -welcome! Or buy the GOG.com versions, which put save files in `~/AppData/Local/GOG.com`. ;-) +There's a few of these. I'm keeping track of them in `unqualified_files.csv` and `unqualified_directories.csv`. Support coming eventually. diff --git a/list.csv b/list.csv index 97ff6ec..47be79f 100644 --- a/list.csv +++ b/list.csv @@ -1,100 +1,104 @@ -".Anodyne", ".Anodyne" -"T-Engine", "T-Engine" -"AppData\Local\Criterion Games", "Criterion Games" -"AppData\Local\GOG.com", "GOG.com" -"AppData\Local\Divinity 2", "Divinity 2" -"AppData\Local\raidenlegacy", "raidenlegacy" -"AppData\Local\Roguelight", "Roguelight" -"AppData\Local\Saved Games", "AppData#Local#Saved Games" -"AppData\Local\UNDERTALE", "UNDERTALE" -"AppData\Roaming\.minecraft", "minecraft" -"AppData\Roaming\AtomZombieData", "AtomZombieData" -"AppData\Roaming\Beat Hazard", "Beat Hazard" -"AppData\Roaming\Broken Rules", "Broken Rules" -"AppData\Roaming\Carbon", "Carbon" -"AppData\Roaming\Dynamite Jack", "Dynamite Jack" -"AppData\Roaming\Faerie Solitaire", "Faerie Solitaire" -"AppData\Roaming\FatShark", "FatShark" -"AppData\Roaming\GridRunnerRev", "GridRunnerRev" -"AppData\Roaming\Irukandji", "Irukandji" -"AppData\Roaming\Little Inferno", "Little Inferno" -"AppData\Roaming\MinMaxGames", "MinMaxGames" -"AppData\Roaming\Nifflas", "Nifflas" -"AppData\Roaming\Scoregasm", "Scoregasm" -"AppData\Roaming\SpaceGiraffe", "SpaceGiraffe" -"AppData\Roaming\StarseedPilgrim", "StarseedPilgrim" -"AppData\Roaming\Voxatron", "Voxatron" -"AppData\Roaming\digipen", "digipen" -"AppData\Roaming\fairybloomre", "fairybloomre" -"AppData\Roaming\offspringfling", "offspringfling" -"Documents\AGK", "AGK" -"Documents\Almost Human", "Almost Human" -"Documents\Alpha Protocol", "Alpha Protocol" -"Documents\Assassin's Creed III", "Assassin's Creed III" -"Documents\Battlefield 3", "Battlefield 3" -"Documents\BioWare", "BioWare" -"Documents\BloodBowlLegendary", "BloodBowlLegendary" -"Documents\ContraptionMaker", "ContraptionMaker" -"Documents\Criterion Games", "Criterion Games" -"Documents\Dungeon of the Endless", "Dungeon of the Endless" -"Documents\EXPLODEMON!", "EXPLODEMON!" -"Documents\Eador", "Eador" -"Documents\Endless Legend", "Endless Legend" -"Documents\Eidos", "Eidos" -"Documents\Elder Scrolls Online", "Elder Scrolls Online" -"Documents\Electronic Arts", "Electronic Arts" -"Documents\Escape Goat", "Escape Goat" -"Documents\Facepalm Games", "Facepalm Games" -"Documents\Full Bore", "Full Bore" -"Documents\Games for Windows - LIVE Demos", "Games for Windows - LIVE Demos" -"Documents\Gaslamp Games", "Gaslamp Games" -"Documents\Giana Sisters - Twisted Dreams", "Giana Sisters - Twisted Dreams" -"Documents\Guacamelee", "Guacamelee" -"Documents\Guild Wars 2", "Guild Wars 2" -"Documents\Heroes of the Storm", "Heroes of the Storm" -"Documents\Klei", "Klei" -"Documents\Larian Studios", "Larian Studios" -"Documents\League of Legends", "League of Legends" -"Documents\ManiaPlanet", "ManiaPlanet" -"Documents\Metanet", "Metanet" -"Documents\MGR", "MGR" -"Documents\Might & Magic Heroes VI", "Might & Magic Heroes VI" -"Documents\My Curse", "My Curse" -"Documents\My Games", "My Games" -"Documents\NBGI", "NBGI" -"Documents\NIGORO", "NIGORO" -"Documents\NeocoreGames", "NeocoreGames" -"Documents\Neverwinter Nights 2", "Neverwinter Nights 2" -"Documents\Nexus Mod Manager", "Nexus Mod Manager" -"Documents\OCTGN", "OCTGN" -"Documents\Over the Top Games", "Over the Top Games" -"Documents\Overlord", "Overlord" -"Documents\Overwatch", "Overwatch" -"Documents\PineappleSmashCrew", "PineappleSmashCrew" -"Documents\Paradox Interactive", "Paradox Interactive" -"Documents\Prince of Persia", "Prince of Persia" -"Documents\PVZ Garden Warfare", "PVZ Garden Warfare" -"Documents\Rayman Legends", "Rayman Legends" -"Documents\Respawn", "Respawn" -"Documents\Rise of the Tomb Raider", "Rise of the Tomb Raider" -"Documents\Robot Entertainment", "Robot Entertainment" -"Documents\SART", "SART" -"Documents\Saved Games", "Saved Games" -"Documents\SavedGames", "SavedGames" -"Documents\SEGA Genesis Classics", "SEGA Genesis Classics" -"Documents\SEGA Mega Drive Classics", "SEGA Mega Drive Classics" -"Documents\Shiner", "Shiner" -"Documents\Snapshot", "Snapshot" -"Documents\Square Enix", "Square Enix" -"Documents\StarCraft", "StarCraft" -"Documents\StarCraft II", "StarCraft II" -"Documents\StarCraft II Beta", "StarCraft II Beta" -"Documents\STAR WARS Battlefront", "STAR WARS Battlefront" -"Documents\Telltale Games", "Telltale Games" -"Documents\Tembo The Badass Elephant", "Tembo The Badass Elephant" -"Documents\Tribute Games", "Tribute Games" -"Documents\Tomb Raider - Legend", "Tomb Raider - Legend" -"Documents\UnrealTournament", "UnrealTournament" -"Documents\WB Games", "WB Games" -"Documents\Witcher 2", "Witcher 2" -"Documents\Wizards of the Coast", "Wizards of the Coast" +source,destination +".Anodyne",".Anodyne" +"T-Engine","T-Engine" +"AppData\Local\Criterion Games","Criterion Games" +"AppData\Local\GOG.com","GOG.com" +"AppData\Local\Divinity 2","Divinity 2" +"AppData\Local\raidenlegacy","raidenlegacy" +"AppData\Local\Roguelight","Roguelight" +"AppData\Local\Saved Games","AppData#Local#Saved Games" +"AppData\Local\UNDERTALE","UNDERTALE" +"AppData\Local\ZeroRanger","ZeroRanger" +"AppData\Roaming\.minecraft","minecraft" +"AppData\Roaming\AtomZombieData","AtomZombieData" +"AppData\Roaming\Baba_Is_You","Baba_Is_You" +"AppData\Roaming\Beat Hazard","Beat Hazard" +"AppData\Roaming\Broken Rules","Broken Rules" +"AppData\Roaming\Carbon","Carbon" +"AppData\Roaming\Dynamite Jack","Dynamite Jack" +"AppData\Roaming\Faerie Solitaire","Faerie Solitaire" +"AppData\Roaming\FatShark","FatShark" +"AppData\Roaming\GridRunnerRev","GridRunnerRev" +"AppData\Roaming\Irukandji","Irukandji" +"AppData\Roaming\Little Inferno","Little Inferno" +"AppData\Roaming\MinMaxGames","MinMaxGames" +"AppData\Roaming\Nifflas","Nifflas" +"AppData\Roaming\Scoregasm","Scoregasm" +"AppData\Roaming\SpaceGiraffe","SpaceGiraffe" +"AppData\Roaming\StarseedPilgrim","StarseedPilgrim" +"AppData\Roaming\Voxatron","Voxatron" +"AppData\Roaming\digipen","digipen" +"AppData\Roaming\fairybloomre","fairybloomre" +"AppData\Roaming\offspringfling","offspringfling" +"Documents\AGK","AGK" +"Documents\Almost Human","Almost Human" +"Documents\Alpha Protocol","Alpha Protocol" +"Documents\Assassin's Creed III","Assassin's Creed III" +"Documents\Battlefield 3","Battlefield 3" +"Documents\BioWare","BioWare" +"Documents\BloodBowlLegendary","BloodBowlLegendary" +"Documents\ContraptionMaker","ContraptionMaker" +"Documents\Criterion Games","Criterion Games" +"Documents\Dungeon of the Endless","Dungeon of the Endless" +"Documents\EXPLODEMON!","EXPLODEMON!" +"Documents\Eador","Eador" +"Documents\Endless Legend","Endless Legend" +"Documents\Eidos","Eidos" +"Documents\Elder Scrolls Online","Elder Scrolls Online" +"Documents\Electronic Arts","Electronic Arts" +"Documents\Escape Goat","Escape Goat" +"Documents\Facepalm Games","Facepalm Games" +"Documents\Full Bore","Full Bore" +"Documents\Games for Windows - LIVE Demos","Games for Windows - LIVE Demos" +"Documents\Gaslamp Games","Gaslamp Games" +"Documents\Giana Sisters - Twisted Dreams","Giana Sisters - Twisted Dreams" +"Documents\Guacamelee","Guacamelee" +"Documents\Guild Wars 2","Guild Wars 2" +"Documents\Heroes of the Storm","Heroes of the Storm" +"Documents\Marvel's Spider-Man Remastered","Marvel's Spider-Man Remastered" +"Documents\Klei","Klei" +"Documents\Larian Studios","Larian Studios" +"Documents\League of Legends","League of Legends" +"Documents\ManiaPlanet","ManiaPlanet" +"Documents\Metanet","Metanet" +"Documents\MGR","MGR" +"Documents\Might & Magic Heroes VI","Might & Magic Heroes VI" +"Documents\My Curse","My Curse" +"Documents\My Games","My Games" +"Documents\NBGI","NBGI" +"Documents\NIGORO","NIGORO" +"Documents\NeocoreGames","NeocoreGames" +"Documents\Neverwinter Nights 2","Neverwinter Nights 2" +"Documents\Nexus Mod Manager","Nexus Mod Manager" +"Documents\OCTGN","OCTGN" +"Documents\Over the Top Games","Over the Top Games" +"Documents\Overlord","Overlord" +"Documents\Overwatch","Overwatch" +"Documents\PineappleSmashCrew","PineappleSmashCrew" +"Documents\Paradox Interactive","Paradox Interactive" +"Documents\Prince of Persia","Prince of Persia" +"Documents\PVZ Garden Warfare","PVZ Garden Warfare" +"Documents\Rayman Legends","Rayman Legends" +"Documents\Respawn","Respawn" +"Documents\Rise of the Tomb Raider","Rise of the Tomb Raider" +"Documents\Robot Entertainment","Robot Entertainment" +"Documents\SART","SART" +"Documents\Saved Games","Saved Games" +"Documents\SavedGames","SavedGames" +"Documents\SEGA Genesis Classics","SEGA Genesis Classics" +"Documents\SEGA Mega Drive Classics","SEGA Mega Drive Classics" +"Documents\Shiner","Shiner" +"Documents\Snapshot","Snapshot" +"Documents\Square Enix","Square Enix" +"Documents\StarCraft","StarCraft" +"Documents\StarCraft II","StarCraft II" +"Documents\StarCraft II Beta","StarCraft II Beta" +"Documents\STAR WARS Battlefront","STAR WARS Battlefront" +"Documents\Telltale Games","Telltale Games" +"Documents\Tembo The Badass Elephant","Tembo The Badass Elephant" +"Documents\Tribute Games","Tribute Games" +"Documents\Tomb Raider - Legend","Tomb Raider - Legend" +"Documents\UnrealTournament","UnrealTournament" +"Documents\WB Games","WB Games" +"Documents\Witcher 2","Witcher 2" +"Documents\Wizards of the Coast","Wizards of the Coast" diff --git a/options.py b/options.py deleted file mode 100644 index 31c73d8..0000000 --- a/options.py +++ /dev/null @@ -1 +0,0 @@ -steamlibrary = "H:\SteamLibrary\SteamApps\common" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e8c1333..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -ntfsutils==0.1.3 \ No newline at end of file diff --git a/save-central.py b/save-central.py deleted file mode 100644 index bff4f9b..0000000 --- a/save-central.py +++ /dev/null @@ -1,81 +0,0 @@ -import shutil -import csv -import os -import sys -import re -import ctypes -from subprocess import call, check_output, STDOUT -from ntfsutils import junction -import options - -def main(): - if (sys.version_info[0] < 3) or (sys.version_info[0] == 3 and sys.version_info[1] < 2): - sys.exit("This script must be run with Python 3.2 or greater") - - create_junctions() - print('Done') - -def create_junctions(): - for old_path, new_path in save_paths(): - move_and_junction(old_path, new_path) - -def save_paths(): - with open('list.csv', newline='') as csvfile: - paths = csv.reader(csvfile, delimiter=',', quotechar='"', - skipinitialspace=True, strict=True) - for old_path, new_path in paths: - old_path = os.path.expanduser("~\\{}".format(old_path)) - new_path = os.path.expanduser("~\\Saved Games\\{}".format(new_path)) - yield old_path, new_path - - with open('unqualified_list.csv', newline='') as csvfile: - paths = csv.reader(csvfile, delimiter=',', quotechar='"', - skipinitialspace=True, strict=True) - for old_path, new_path in paths: - old_path = os.path.expandvars(old_path) - old_path = re.sub("%steamlibrary%", options.steamlibrary, old_path) - new_path = os.path.expanduser("~\\Saved Games\\{}".format(new_path)) - yield old_path, new_path - -def move_and_junction(old_path, new_path): - if os.path.exists(old_path): - if not junction.isjunction(old_path): - if os.path.exists(new_path): - print("Can't move {}".format(old_path)) - print("Conflicting folder already exists: {}".format(new_path)) - return - move_save(old_path, new_path) - link_save(old_path, new_path) - hide_directory(old_path) - else: - print("Already junctioned: {}".format(old_path)) - -def restore_junctions(): - for old_path, new_path in save_paths(): - restore_junction(old_path, new_path) - -def restore_junction(old_path, new_path): - if os.path_exists(new_path): - if os.path_exists(old_path): - if junction.isjunction(old_path): - print("Already junctioned: {}".format(old_path)) - else: - print("Cannot junction; conflicting folder already exists: {}".format(old_path)) - else: - link_save(old_path, new_path) - hide_directory(old_path) - -def move_save(old_path, new_path): - print("Moving... {} -> {}".format(old_path, new_path)) - shutil.move(old_path, new_path) - -def link_save(old_path, new_path): - print("Linking... {} -> {}".format(old_path, new_path)) - junction.create(new_path, old_path) - -def hide_directory(path): - ctypes.windll.kernel32.SetFileAttributesW(path, 2) - -if __name__ == "__main__": - main() - diff --git a/smol_list.csv b/smol_list.csv new file mode 100644 index 0000000..0c8ec16 --- /dev/null +++ b/smol_list.csv @@ -0,0 +1,2 @@ +source, dest +"AppData\Roaming\Baba_Is_You","Baba_Is_You" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..33bae72 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,108 @@ +// use std::fs; +use csv::Error; +use std::{env::var_os, path::PathBuf}; +use std::path::Path; +use std::ffi::CString; +use std::os::raw::c_char; + +pub fn get_userprofile() -> String { + match var_os("USERPROFILE").map(PathBuf::from) { + Some(path) => { + return path.to_string_lossy().to_string(); + } + None => { + panic!("Path does not exist"); + } + } +} + +fn main() -> Result<(), Error> { + let mut reader = csv::Reader::from_path("list.csv")?; + for record in reader.records() { + let record = record?; + let from = format!("{}\\{}", get_userprofile(), &record[0]); + let to = format!("{}\\Saved Games\\{}", get_userprofile(), &record[1]); + let from_path = Path::new(&from); + let to_path = Path::new(&to); + + if from_path.exists() { + if is_junction(&from) { + println!("✅ {}", from); + } else { + if to_path.exists() { + println!("⚠️ Can't move {} -> {}", from, to); + println!("Folder in sync directory already exists"); + } else { + println!("🔗 Moving and junctioning {} -> {}", from, to); + move_and_junction(&from, &to); + } + } + } else if to_path.exists() { + if from_path.exists() { + println!("⚠️ Can't link {} -> {}", to, from); + println!("from sync directory. Save data at default location already exists."); + } else { + println!("🔗 Restoring junction {} -> {}", from, to); + restore_junction(&from, &to); + } + } + } + + Ok(()) +} + +fn is_junction(path: &str) -> bool { + let is_junction = junction::exists(path); + + match is_junction { + Ok(true) => return true, + Ok(false) => return false, + Err(e) => { + match e.raw_os_error() { + Some(4390) => return false, // Not a junction + _ => panic!("Problem checking the directory: {:?}", e) + } + } + } +} + +fn move_and_junction(old_path: &str, new_path: &str) { + move_save(old_path, new_path); + link_save(new_path, old_path); + hide_directory(old_path); +} + +fn restore_junction(normal_path: &str, save_path: &str) { + link_save(save_path, normal_path); + hide_directory(normal_path); +} + +fn create_junction(target: &str, junction: &str) { + let _ = junction::create(target, junction); +} + +fn move_save(old_path: &str, new_path: &str) { + let result = std::fs::rename(old_path, new_path); + match result { + Ok(()) => return, + Err(e) => { + panic!("Problem renaming the directory: {:?}", e) + } + } +} + +fn link_save(target: &str, junction: &str) { + create_junction(target, junction); +} + +#[cfg(windows)] +fn hide_directory(path: &str) { + let c_path = CString::new(path).unwrap(); + let c_path_ptr: *const c_char = c_path.as_ptr() as *const c_char; + unsafe { + winapi::um::fileapi::SetFileAttributesA( + c_path_ptr, + winapi::um::winnt::FILE_ATTRIBUTE_HIDDEN + ); + } +} diff --git a/unqualified_list.csv b/unqualified_directories.csv similarity index 100% rename from unqualified_list.csv rename to unqualified_directories.csv