├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── sequestrum ├── __init__.py ├── arguments.py ├── directories.py ├── logging.py ├── options.py ├── sequestrum.py └── symlink.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg[s] 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Environments 51 | .env 52 | .venv 53 | env/ 54 | venv/ 55 | ENV/ 56 | env.bak/ 57 | venv.bak/ 58 | 59 | # IDEs 60 | .vscode/ 61 | 62 | # mypy 63 | .mypy_cache/ 64 | .dmypy.json 65 | dmypy.json 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | **Note**: contributing implies that your contributions get licensed under the terms of [LICENSE.md](LICENSE.md) (MIT). 4 | 5 | ## Opening issues 6 | 7 | 1. Make sure you have a GitHub account. 8 | 2. Include steps to reproduce, if it is a bug. 9 | 3. Include information on what version you are using. 10 | 11 | ## Submit changes 12 | 13 | Commit messages should be both _clear_ and _descriptive_. If possible they should start with the _verb_ that describes the change. 14 | Don't be shy to include additional information, like motivation, for that change below the initial line. 15 | 16 | Other developers should be able to understand _why_ a change occurred, when looking at it at a later point in time. 17 | 18 | ### Coding Style 19 | 20 | This project follows the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. 21 | Make sure you adhere to the coding style, or your pull request might not get merged. 22 | 23 | --- 24 | 25 | That said, if anything is unclear or I missed something feel free to open an issue. 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ivy Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Sequestrum

3 |
4 | 5 | ## Important 6 | No longer being maintained, we'll see if I start work on this again. 7 | 8 | ## Description 9 | A modern, lightweight dotfile manager for the masses. This README.md may look daunting, but trust me on this, it's as easy as pie. 10 | Promise. Now you may be wondering, why use this over Stow or Dotbot. Simple.Well for starters, the name is better. Do you need any 11 | other reasons? Fine, fine.. some real reasons.This is specifically made for dotfile management and provides features like modularity, 12 | dotfile repository setup, and more! 13 | 14 | ## Install Guide 15 | - Arch: `yay -S sequestrum-git` 16 | - Pip: `pip install --user sequestrum` 17 | 18 | ## Wiki 19 | For help on writing a config and setting up Sequestrum, visit the [Wiki](https://github.com/iiPlasma/sequestrum/wiki) 20 | 21 | ## Contributing 22 | If you'd like to contribute, whether that'd be improving the comments or README (which could always use work), or adding functionality, 23 | just fork this, add to it, then send a merge request. I'll try and get around to looking at it as soon as possible. 24 | Be sure to read [CONTRIBUTING.md](CONTRIBUTING.md) first though. 25 | 26 | ## License 27 | Copyright (c) 2018-2018 Ivy Zhang. Released under the MIT License. 28 | -------------------------------------------------------------------------------- /sequestrum/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iyzg/sequestrum/fdcdc244603a00aeb5fa8537c4edc5ed05be329c/sequestrum/__init__.py -------------------------------------------------------------------------------- /sequestrum/arguments.py: -------------------------------------------------------------------------------- 1 | # Arguments Modules 2 | 3 | # Imports 4 | import argparse 5 | 6 | 7 | def get_arguments(): 8 | """ 9 | Return the arguments in the form of a tuple 10 | """ 11 | 12 | parser = argparse.ArgumentParser() 13 | group = parser.add_mutually_exclusive_group() 14 | 15 | group.add_argument("-i", "--install", help="Install packages onto local system. Use all to install all packages.") 16 | group.add_argument("-r", "--refresh", help="Refresh your dotfiles based on your config.", action="store_true") 17 | group.add_argument("-u", "--unlink", help="Unlink packages from local system. Use all to unlink all packages.") 18 | 19 | args = parser.parse_args() 20 | 21 | if args.install is not None: 22 | return ("Install", args.install) 23 | elif args.refresh: 24 | return ("Refresh", "all") 25 | elif args.unlink is not None: 26 | return ("Unlink", args.unlink) 27 | -------------------------------------------------------------------------------- /sequestrum/directories.py: -------------------------------------------------------------------------------- 1 | # Directory Module 2 | 3 | # Libraries 4 | import os 5 | import shutil 6 | import pathlib 7 | import sequestrum.logging as logging 8 | 9 | 10 | def current_path(): 11 | """ 12 | Returns the current path 13 | """ 14 | 15 | return os.getcwd() 16 | 17 | 18 | def create_folder(path): 19 | """ 20 | Creates a folder 21 | """ 22 | 23 | try: 24 | os.makedirs(path) 25 | except OSError as error: 26 | logging.print_error("Could not create folder \"{}\" due to following error: {}".format(path, error)) 27 | else: 28 | logging.print_verbose("Folder doesn't exist and was created: {}".format(path)) 29 | 30 | 31 | def create_base_folder(path): 32 | """ 33 | Create base directory if needed 34 | """ 35 | 36 | basePath = pathlib.Path(path).parent 37 | 38 | # Check if the base folder is a file 39 | if basePath.exists(): 40 | # Check if the parent is a file or if its a symlink 41 | if basePath.is_file() or basePath.is_symlink(): 42 | logging.print_error("Base directory is a file or link: {}".format(basePath)) 43 | return False 44 | # If not, it must be a directory, so we are ok 45 | else: 46 | return True 47 | 48 | # Create path and parents (or ignore if folder already exists) 49 | try: 50 | basePath.mkdir(parents=True, exist_ok=True) 51 | except Exception as error: 52 | logging.print_error("Could not create parent folder \"{}\" due to following error: {}".format(basePath, error)) 53 | return False 54 | else: 55 | logging.print_verbose("Parent folder dosent exist and was created: {}".format(basePath)) 56 | 57 | return True 58 | 59 | 60 | def delete_folder(path): 61 | """ 62 | Deletes a folder 63 | """ 64 | 65 | basePath = pathlib.Path(path) 66 | 67 | if not basePath.exists(): 68 | logging.print_error("Folder doesn't exist: {}".format(path)) 69 | return 70 | 71 | try: 72 | if basePath.is_symlink(): 73 | basePath.unlink() 74 | else: 75 | shutil.rmtree(basePath) 76 | except OSError as error: 77 | logging.print_error("Folder Deletion/Unsymlink Failed: {}".format(error)) 78 | else: 79 | logging.print_verbose("Folder Deleted Successfully: {}".format(path)) 80 | 81 | 82 | def delete_file(path): 83 | """ 84 | Deletes file 85 | """ 86 | 87 | try: 88 | os.remove(path) 89 | except OSError as error: 90 | logging.print_error("File Deletion Failed: {}".format(path)) 91 | else: 92 | logging.print_verbose("File Successfully Deleted: {}".format(path)) 93 | 94 | 95 | def is_folder(path): 96 | """ 97 | Checks to see if path is a folder 98 | """ 99 | 100 | try: 101 | if os.path.isdir(path): 102 | return True 103 | else: 104 | return False 105 | except OSError as error: 106 | logging.print_error("Path doesn't exist: {}".format(path)) 107 | 108 | 109 | def is_file(path): 110 | """ 111 | Checks to see if path is a file 112 | """ 113 | 114 | try: 115 | if os.path.isfile(path): 116 | return True 117 | else: 118 | return False 119 | except OSError as error: 120 | logging.print_error("Path doesn't exist: {}".format(path)) 121 | 122 | 123 | def grab_package_names(path): 124 | """ 125 | Grabs package names from config 126 | """ 127 | 128 | package_list = [] 129 | for name in os.listdir(path): 130 | if os.path.isdir(path): 131 | package_list.append(name) 132 | 133 | return package_list 134 | -------------------------------------------------------------------------------- /sequestrum/logging.py: -------------------------------------------------------------------------------- 1 | # Logging module 2 | 3 | from sys import exit 4 | 5 | 6 | def format_output(error_type, error_message): 7 | """ 8 | Formats the errors to look standard 9 | """ 10 | return "[{}] {}".format(error_type, error_message) 11 | 12 | 13 | def print_fatal(error_message): 14 | """ 15 | Prints message with FATAL 16 | """ 17 | print("\033[1;31mFATAL\033[0m {}".format(error_message)) 18 | exit() 19 | 20 | 21 | def print_error(error_message): 22 | """ 23 | Prints message with ERROR 24 | """ 25 | print("\033[1;31mERROR\033[0m {}".format(error_message)) 26 | 27 | 28 | def print_warn(error_message): 29 | """ 30 | Prints message with WARN 31 | """ 32 | print("\033[1;33mWARN\033[0m {}".format(error_message)) 33 | 34 | 35 | def print_info(error_message): 36 | """ 37 | Prints message with INFO 38 | """ 39 | print("\033[1;32mINFO\033[0m {}".format(error_message)) 40 | 41 | 42 | def print_verbose(error_message): 43 | """ 44 | Prints message with green color to show success 45 | """ 46 | print("\033[1;32mVERBOSE\033[0m {}".format(error_message)) 47 | -------------------------------------------------------------------------------- /sequestrum/options.py: -------------------------------------------------------------------------------- 1 | # Options Module 2 | 3 | # Imports 4 | from subprocess import run 5 | import sequestrum.logging as logging 6 | 7 | 8 | def run_commands(unparsed_command_list, package_name=None): 9 | """ 10 | Runs commands passed in 11 | """ 12 | 13 | for command in unparsed_command_list: 14 | parsed_command = command.split() 15 | 16 | try: 17 | runner = run(parsed_command) 18 | except Exception as error: 19 | logging.print_fatal("Error occured during command \"{}\": {}".format(command, error), package_name) 20 | else: 21 | logging.print_verbose("Command \"{}\" finished with exit code: {}".format(command, runner.returncode), package_name) 22 | -------------------------------------------------------------------------------- /sequestrum/sequestrum.py: -------------------------------------------------------------------------------- 1 | # Libraries 2 | from pathlib import Path 3 | import sys 4 | import yaml 5 | 6 | # Modules 7 | import sequestrum.directories as directories 8 | import sequestrum.symlink as symlink 9 | import sequestrum.arguments as arguments 10 | import sequestrum.options as options 11 | import sequestrum.logging as logging 12 | 13 | # For Later 14 | packages_to_unlink = [] 15 | 16 | # Global constants 17 | HOME_PATH = str(Path.home()) + "/" 18 | 19 | 20 | # Creates a new directory. It creates a new folder path using the config 21 | # then creates a new folder using that path. It then loops through each 22 | # link in the links list and **copies** (not symlinking) the original file 23 | # on the source system over to the dotfiles. 24 | 25 | 26 | def setup_package(package_key, config_dict, dotfile_path): 27 | """ 28 | Setup package directory on dotfile 29 | """ 30 | 31 | # Make a path for the new directory path using the name specified in the 32 | # config then make the folder using the path. 33 | package_config = config_dict['options'][package_key] 34 | new_package_path = dotfile_path + package_config['directoryName'] + "/" 35 | if directories.is_folder(new_package_path) is False: 36 | directories.create_folder(new_package_path) 37 | 38 | for link in package_config['links']: 39 | for key, value in link.items(): 40 | source_file = HOME_PATH + value 41 | dest_file = new_package_path + key 42 | 43 | # Checks 44 | if directories.is_folder(dest_file): 45 | logging.print_warn("Folder exists, skipping: {}".format(dest_file)) 46 | continue 47 | elif directories.is_file(dest_file): 48 | logging.print_warn("File exists, skipping: {}".format(dest_file)) 49 | continue 50 | 51 | # Setup 52 | if directories.is_folder(source_file): 53 | symlink.copy_folder(source_file, dest_file) 54 | directories.delete_folder(source_file) 55 | elif directories.is_file(source_file): 56 | symlink.copy_file(source_file, dest_file) 57 | directories.delete_file(source_file) 58 | else: 59 | return False 60 | 61 | return True 62 | 63 | 64 | # Grabs the directory of the key. For each item in the dotfile directory, 65 | # properly symlink the file to the right place. If the file on the local 66 | # system already exists. Delete the existing file before symlinking to 67 | # prevent issues. 68 | 69 | 70 | def install_package(package_key, config_dict, dotfile_path): 71 | """ 72 | Install package to local system 73 | """ 74 | 75 | # Grab dotfile package directory 76 | package_config = config_dict['options'][package_key] 77 | directory_path = dotfile_path + package_config['directoryName'] + "/" 78 | 79 | # Loop through files to link 80 | for link in package_config['links']: 81 | # Symlink files to local files 82 | for key, value in link.items(): 83 | source_file = directory_path + key 84 | dest_file = HOME_PATH + value 85 | 86 | if directories.is_folder(dest_file): 87 | logging.print_warn("Folder exists, skipping: {}".format(dest_file)) 88 | continue 89 | elif directories.is_file(dest_file): 90 | logging.print_warn("File exists, skipping: {}".format(dest_file)) 91 | continue 92 | 93 | if directories.create_base_folder(dest_file): 94 | symlink.create_symlink(source_file, dest_file) 95 | else: 96 | return False 97 | 98 | return True 99 | 100 | 101 | def get_packages_to_unlink(package_key, config_dict, dotfile_path): 102 | """ 103 | Grab packages and put them into a list ( NO DUPES ) 104 | """ 105 | 106 | package_config = config_dict['options'][package_key] 107 | 108 | for link in package_config['links']: 109 | for _, value in link.items(): 110 | fileToGrab = HOME_PATH + value 111 | 112 | if fileToGrab not in packages_to_unlink: 113 | packages_to_unlink.append(fileToGrab) 114 | 115 | 116 | def unlink_packages(): 117 | """ 118 | Unlink all files in packages_to_unlink 119 | """ 120 | for path in packages_to_unlink: 121 | if directories.is_folder(path): 122 | directories.delete_folder(path) 123 | elif directories.is_file(path): 124 | directories.delete_file(path) 125 | else: 126 | logging.print_error("Nothing to unlink") 127 | 128 | # Goes through all the file locations that need to be empty for the 129 | # symlinking to work and checks to see if they're empty. If they're not, 130 | # it will return false. If it is clean, it'll return true. 131 | 132 | 133 | def check_localfile_locations(package_key, config_dict, mode=None): 134 | """ 135 | Checks local file locations 136 | """ 137 | 138 | if mode is None: 139 | logging.print_fatal("No mode provided to check") 140 | 141 | for link in config_dict['options'][package_key]['links']: 142 | for key, value in link.items(): 143 | destPath = HOME_PATH + value 144 | 145 | if mode == "Clean": 146 | if symlink.symlink_source_exists(destPath): 147 | logging.print_fatal("Local file location occupied: {}".format(destPath)) 148 | return False 149 | elif mode == "Dirty": 150 | if not symlink.symlink_source_exists(destPath): 151 | logging.print_fatal("Local file location empty: {}".format(destPath)) 152 | return False 153 | 154 | return True 155 | 156 | # Checks to see if the file locations in the dotfile repository exist. If 157 | # they do, return false. If they don't, return true. This is to prevent 158 | # overwriting of file that may or may not be important to the user. 159 | 160 | 161 | def check_dotfile_locations(package_key, config_dict, dotfile_path, mode=None): 162 | """ 163 | Checks dotfile locations 164 | """ 165 | 166 | if mode is None: 167 | logging.print_fatal("No mode provided to check") 168 | 169 | directory_path = dotfile_path + \ 170 | config_dict['options'][package_key]['directoryName'] + "/" 171 | 172 | for link in config_dict['options'][package_key]['links']: 173 | for key, value in link.items(): 174 | sourcePath = directory_path + key 175 | 176 | if mode == "Clean": 177 | if symlink.symlink_source_exists(sourcePath): 178 | logging.print_fatal("Dotfile location occupied: {}".format(sourcePath)) 179 | return False 180 | if mode == "Dirty": 181 | if not symlink.symlink_source_exists(sourcePath): 182 | logging.print_fatal("Dotfile location empty: {}".format(sourcePath)) 183 | return False 184 | 185 | return True 186 | 187 | 188 | def main(): 189 | # Grab user inputted args from the module and make sure they entered some. 190 | args = arguments.get_arguments() 191 | 192 | if args is None: 193 | logging.print_error("Must pass arguments") 194 | sys.exit() 195 | 196 | config_file = None 197 | config_dict = {} 198 | package_list = [] 199 | 200 | try: 201 | config_file = open("config.yaml", "r") 202 | except IOError: 203 | logging.print_error("No configuration found") 204 | sys.exit() 205 | 206 | config_dict = yaml.load(config_file) 207 | 208 | # Grab list of directories from the config. 209 | for key, value in config_dict['options'].items(): 210 | if key.endswith("Package"): 211 | friendly_name = key[:-7] 212 | config_dict['options'][key]['package_name'] = friendly_name 213 | package_list.append(friendly_name) 214 | 215 | # Error checking for proper config 216 | if "base" not in config_dict['options']: 217 | logging.print_fatal( 218 | "Invalid config file, a base package needs to be defined") 219 | elif "dotfileDirectory" not in config_dict['options']['base']: 220 | logging.print_fatal("Missing dotfileDirectory in base package") 221 | 222 | # Grab the path of the dotfile directory 223 | dotfile_path = HOME_PATH + \ 224 | config_dict['options']['base']['dotfileDirectory'] + "/" 225 | 226 | # Install the files from the dotfiles. Symlinks the files from the 227 | # specified packages to the local system files. If the file or folder 228 | # already exists on the local system, delete it then symlink properly to 229 | # avoid errors. 230 | if args[0] == "Install": 231 | # Install all packages 232 | if args[1] == "all": 233 | for key, value in config_dict['options'].items(): 234 | if key.endswith("Package"): 235 | check_dotfile_locations(key, config_dict, dotfile_path, "Dirty") 236 | 237 | for key, value in config_dict['options'].items(): 238 | if key.endswith("Package"): 239 | if "commandsBefore" in value: 240 | options.run_commands( 241 | config_dict['options'][key]['commandsBefore'], config_dict['options'][key]['package_name']) 242 | install_package(key, config_dict, dotfile_path) 243 | if "commandsAfter" in value: 244 | options.run_commands( 245 | config_dict['options'][key]['commandsAfter'], config_dict['options'][key]['package_name']) 246 | 247 | logging.print_info("Complete installation complete") 248 | 249 | # The option to only install one package instead of all your dotfiles. 250 | elif args[1] in package_list: 251 | for key, value in config_dict['options'].items(): 252 | if key == args[1] + "Package": 253 | check_dotfile_locations(key, config_dict, dotfile_path, "Dirty") 254 | 255 | for key, value in config_dict['options'].items(): 256 | if key == args[1] + "Package": 257 | if "commandsBefore" in value: 258 | options.run_commands( 259 | config_dict['options'][key]["commandsBefore"]) 260 | install_package(key, config_dict, dotfile_path) 261 | if "commandsAfter" in value: 262 | options.run_commands( 263 | config_dict['options'][key]["commandsAfter"]) 264 | 265 | logging.print_info("{} installation complete".format(args[1])) 266 | else: 267 | logging.print_error("Invalid Package") 268 | 269 | # Refresh 270 | # ------- 271 | # Refreshs the symlinks with the current file. This is so users don't have to manually 272 | # move files around and can instead manage their dotfiles in one file. 273 | # TODO: Remove files with warning if they are gone from the config 274 | elif args[0] == "Refresh": 275 | if args[1] == "all": 276 | for key, value in config_dict['options'].items(): 277 | if key.endswith("Package"): 278 | if "commandsBefore" in value: 279 | options.run_commands( 280 | config_dict['options'][key]["commandsBefore"]) 281 | setup_package(key, config_dict, dotfile_path) 282 | if "commandsAfter" in value: 283 | options.run_commands( 284 | config_dict['options'][key]["commandsAfter"]) 285 | 286 | logging.print_info("Dotfile refresh complete") 287 | else: 288 | logging.print_error("Error 102 Please report to GH") 289 | 290 | # Unlink the source files. This doesn't really "unlink", instead it actually just 291 | # deletes the files. It collects a list of files to unlink then it goes through and 292 | # unlinks them all. 293 | # TODO: Add safety to make sure both the files exist 294 | elif args[0] == "Unlink": 295 | if args[1] == "all": 296 | for key, value in config_dict['options'].items(): 297 | if key.endswith("Package"): 298 | get_packages_to_unlink(key, config_dict, dotfile_path) 299 | unlink_packages() 300 | logging.print_info("Completele unlink complete") 301 | elif args[1] in package_list: 302 | for key, value in config_dict['options'].items(): 303 | if key == args[1] + "Package": 304 | get_packages_to_unlink(key, config_dict, dotfile_path) 305 | unlink_packages() 306 | logging.print_info("{} unlink complete".format(args[1])) 307 | else: 308 | logging.print_error("Invalid Package") 309 | 310 | else: 311 | logging.print_error("Invalid Command") 312 | 313 | 314 | if __name__ == '__main__': 315 | main() 316 | -------------------------------------------------------------------------------- /sequestrum/symlink.py: -------------------------------------------------------------------------------- 1 | # Symlink Module 2 | 3 | # Libraries 4 | import os 5 | from shutil import copytree, copyfile 6 | import sequestrum.logging as logging 7 | 8 | 9 | def create_symlink(source, destination): 10 | """ 11 | Creates symlink from source to destination 12 | """ 13 | 14 | try: 15 | os.symlink(source, destination) 16 | except OSError as error: 17 | logging.print_error("Unable to create symlink: {}".format(error)) 18 | else: 19 | logging.print_verbose("Linking {} <-> {}".format(source, destination)) 20 | 21 | 22 | def copy_file(source, destination): 23 | """ 24 | Copys file from source to destination 25 | """ 26 | 27 | try: 28 | copyfile(source, destination) 29 | except IOError as error: 30 | logging.print_error("Unable to copy file: {}".format(error)) 31 | else: 32 | logging.print_verbose("Copied {} --> {}".format(source, destination)) 33 | 34 | 35 | def copy_folder(source, destination): 36 | """ 37 | Copies frolder from source to destination 38 | """ 39 | 40 | try: 41 | copytree(source, destination) 42 | except IOError as error: 43 | logging.print_error("Unable to copy folder: {}".format(error)) 44 | else: 45 | logging.print_verbose("Copied {} --> {}".format(source, destination)) 46 | 47 | 48 | def symlink_source_exists(sourcePath): 49 | """ 50 | Checks to see if symlink source exists 51 | """ 52 | 53 | # Check if file exists 54 | if not os.path.exists(sourcePath): 55 | return False 56 | 57 | # We cannot link symlinks 58 | if os.path.islink(sourcePath): 59 | return False 60 | 61 | return os.path.exists(sourcePath) 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='sequestrum', 5 | description='Modern Dotfile Manager', 6 | long_description=open('README.md').read(), 7 | long_description_content_type='text/markdown', 8 | use_scm_version=True, 9 | setup_requires=['setuptools_scm'], 10 | url='http://github.com/iiPlasma/sequestrum', 11 | author='Ivy Zhang', 12 | author_email="ZIvy042003@gmail.com", 13 | license='MIT', 14 | packages=['sequestrum'], 15 | install_requires=[ 16 | 'pyyaml == 5.1', 17 | ], 18 | entry_points={ 19 | 'console_scripts': 20 | ['sequestrum = sequestrum.sequestrum:main'] 21 | } 22 | ) 23 | --------------------------------------------------------------------------------