diff options
| -rwxr-xr-x | .envrc | 12 | ||||
| -rwxr-xr-x | .gitignore | 12 | ||||
| -rwxr-xr-x | CMakeLists.txt | 14 | ||||
| -rwxr-xr-x | LICENSE | 21 | ||||
| -rwxr-xr-x | README.md | 12 | ||||
| -rwxr-xr-x | devenv.lock | 103 | ||||
| -rwxr-xr-x | devenv.nix | 8 | ||||
| -rwxr-xr-x | devenv.yaml | 15 | ||||
| -rw-r--r-- | src/backup.c | 172 | ||||
| -rw-r--r-- | src/config.c | 24 | ||||
| -rw-r--r-- | src/ignore.c | 66 | ||||
| -rwxr-xr-x | src/lib.h | 24 | ||||
| -rwxr-xr-x | src/main.c | 123 |
13 files changed, 606 insertions, 0 deletions
@@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +# `use devenv` supports the same options as the `devenv shell` command. +# +# To silence all output, use `--quiet`. +# +# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true +use devenv diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..60bb522 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml + +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100755 index 0000000..0125655 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.10) +project(backbencher) + +file(GLOB SOURCES "src/**/*.c") +file(GLOB SRC_FILES "src/*.c") +set(SOURCES ${SOURCES} ${SRC_FILES}) + +if(SOURCES) + add_executable(backbencher ${SOURCES}) +else() + message(FATAL_ERROR "No .c files found in source") +endif() + +target_compile_options(backbencher PRIVATE -Ofast) @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Aargh Rai + +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 the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..d326618 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Backbencher + +Backup your files +Usage: Backbencher [OPTIONS] [input] +Arguments: + [input]... The path of the folder to backup +Options: + -i --ignore-file <filename> Specify which files to exclude + (default = ~/.config/backbencher/.backupignore) + -h --help Print help + -v --verbose Verbose logging + -V --version Print version diff --git a/devenv.lock b/devenv.lock new file mode 100755 index 0000000..1dcb611 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1765463314, + "owner": "cachix", + "repo": "devenv", + "rev": "ec805a5ad3e3ffb6d47d05c078a60db2e636fac4", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1765121682, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765464257, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "09e45f2598e1a8499c3594fe11ec2943f34fe509", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762808025, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1764580874, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100755 index 0000000..b8babeb --- /dev/null +++ b/devenv.nix @@ -0,0 +1,8 @@ +{ pkgs, lib, config, inputs, ... }: + +{ + packages = with pkgs; [ git gcc gnumake cmake ]; + enterShell = '' + git --version # Use packages + ''; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100755 index 0000000..116a2ad --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + +# If you're using non-OSS software, you can set allowUnfree to true. +# allowUnfree: true + +# If you're willing to use a package that's vulnerable +# permittedInsecurePackages: +# - "openssl-1.1.1w" + +# If you have more than one devenv you can merge them +#imports: +# - ./backend diff --git a/src/backup.c b/src/backup.c new file mode 100644 index 0000000..d90d1d1 --- /dev/null +++ b/src/backup.c @@ -0,0 +1,172 @@ +#include "lib.h" +#include <dirent.h> +#include <regex.h> + +int check_path_type(const char* path) { + struct stat path_stat; + if (stat(path, &path_stat) != 0) { + return path_type_unknown; + } + if (S_ISDIR(path_stat.st_mode)) { + return path_type_directory; + } + return path_type_file; +} + +int follows_ignore_rules(char* path, char* ignore_line) { + char lastbitofthepath[PATH_MAX]; + int k = 0; + for (int i = 0; path[i] != 0; i++) { + if (path[i] == '/') { + lastbitofthepath[k] = 0; + k = 0; + continue; + } + lastbitofthepath[k++] = path[i]; + } + if (k != 0 && lastbitofthepath[k] != 0) { + lastbitofthepath[k] = 0; + } + + regex_t re; + int ret; + + if (regcomp(&re, ignore_line, REG_EXTENDED) != 0) { + return 0; + } + + ret = regexec(&re, lastbitofthepath, (size_t) 0, NULL, 0); + regfree(&re); + + if (ret == 0) { + return 1; + } else { + return 0; + } +} + +int should_ignore(char* path, char* ignore_instructions, int VERBOSE) { + int i = 0, k = 0; + char line[PATH_MAX]; + while (ignore_instructions[i] != 0) { + if (ignore_instructions[i] != '\n') { + line[k++] = ignore_instructions[i]; + i++; + continue; + } + line[k] = 0; + k = 0; + if (!follows_ignore_rules(path, line)) { + i++; + continue; + } + if (VERBOSE) { + printf("Ignoring %s due to ignore rule %s\n", path, line); + } + return 1; + } + return 0; +} + +void copy_to_backup(char* source_path, int VERBOSE) { + char destination_path[PATH_MAX]; + int index_of_first_slash = -1; + for (int i = 0; i < PATH_MAX; i++) { + if (source_path[i] != '/') continue; + index_of_first_slash = i; + break; + } + + if (index_of_first_slash == -1) { + // I DON'T EVEN KNOW HOW TO HANDLE THIS + fprintf(stderr, "Weird path issue encountered, failed to copy to source\nPath encountered: %s", source_path); + return; + } + + int i = 0; + for (; i < index_of_first_slash; i++) { + destination_path[i] = source_path[i]; + } + destination_path[i++] = '.'; + destination_path[i++] = 'b'; + destination_path[i] = 'k'; + while (++i < PATH_MAX) { + destination_path[i] = source_path[i - 3]; + } + + if (VERBOSE) { + printf("%s -> %s\n", source_path, destination_path); + } + + verify_parent_dirs_exist(destination_path, VERBOSE); + + char command[2 * PATH_MAX + 10]; + snprintf(command, 2 * PATH_MAX + 10, "cp \"%s\" \"%s\"", source_path, destination_path); + system(command); + if (VERBOSE) { + printf("Executing: %s\n", command); + } +} + +void push(char* directories, int cursor, const char* to_add) { + int i = 0; + for (; to_add[i] != 0; i++) { + directories[cursor * PATH_MAX + i] = to_add[i]; + } + while (i < PATH_MAX) { + directories[cursor * PATH_MAX + i++] = 0; + } +} + +void backup(char* target, char* ignore_instructions, int VERBOSE) { + int file_type = check_path_type(target); + if (file_type != path_type_directory) { + fprintf(stderr, "Input not a directory\n"); + return; + } + + int cursor = 0; + char* directories = malloc(PATH_MAX * PATH_MAX * sizeof(*directories)); + push(directories, cursor, target); + while (cursor != -1) { + char* path = &directories[(cursor--) * PATH_MAX]; + DIR* dir_ptr = opendir(path); + if (dir_ptr == NULL) { + fprintf(stderr, "Couldn't open the directory: %s\n", path); + continue; + } + + struct dirent* entry_ptr; + char full_path[PATH_MAX]; + int i = 0; + for (; path[i] != 0; i++) { + full_path[i] = path[i]; + } + full_path[i++] = '/'; + while ((entry_ptr = readdir(dir_ptr)) != NULL) { + if ( + strcmp(entry_ptr->d_name, ".") == 0 || + strcmp(entry_ptr->d_name, "..") == 0 + ) continue; + int j = 0; + for (; entry_ptr->d_name[j] != 0; j++) { + full_path[i + j] = entry_ptr->d_name[j]; + } + full_path[i + j] = 0; + + if (VERBOSE) { + printf("Looking at: %s\n", full_path); + } + if (should_ignore(full_path, ignore_instructions, VERBOSE)) { + continue; + } + + if (entry_ptr->d_type == DT_REG) { + copy_to_backup(full_path, VERBOSE); + continue; + } + push(directories, ++cursor, full_path); + } + closedir(dir_ptr); + } +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..ab0eb05 --- /dev/null +++ b/src/config.c @@ -0,0 +1,24 @@ +#include "lib.h" +#include <stdio.h> +#include <stdlib.h> + +char config[PATH_MAX] = {0}; +char* get_default_config() { + if (config[0] != 0) return config; + + const char* xdg_config_home = getenv("XDG_CONFIG_HOME"); + const char* home = getenv("HOME"); + + if (home == NULL) { + fprintf(stderr, "HOME environment variable not set, failed to determine the config path"); + return NULL; + } + + if (xdg_config_home != NULL && xdg_config_home[0] != 0) { + snprintf(config, PATH_MAX, "%s/backbencher/.backupignore", xdg_config_home); + } else { + snprintf(config, PATH_MAX, "%s/.config/backbencher/.backupignore", home); + } + + return config; +} diff --git a/src/ignore.c b/src/ignore.c new file mode 100644 index 0000000..fbfb8ed --- /dev/null +++ b/src/ignore.c @@ -0,0 +1,66 @@ +#include "lib.h" + +int _; + +void mkdir_if_not_exist(char* path, int VERBOSE) { + if (mkdir(path, S_IRWXU) < 0) return; + if (VERBOSE) { + printf("(Created to create directory %s)", path); + } +} + +void verify_parent_dirs_exist(char* file_path, int VERBOSE) { + for (int i = 0; file_path[i] != 0; i++) { + if (file_path[i] == '/') file_path[i] = 0; + if (file_path[i] != 0) continue; + mkdir_if_not_exist(file_path, VERBOSE); + file_path[i] = '/'; + } +} + +char* get_ignore_file(char* ignore_file, int VERBOSE) { + verify_parent_dirs_exist(ignore_file, VERBOSE); + FILE* file = fopen(ignore_file, "r"); + char* default_config; + + if (file != 0) { + float current_pointer = ftell(file); + fseek(file, 0, SEEK_END); + long int file_size = ftell(file); + fseek(file, current_pointer, SEEK_SET); + + char* contents = malloc(file_size * sizeof(*contents)); + _ = fread(contents, 1, file_size, file); + fclose(file); + + return contents; + } + + if (strcmp(ignore_file, (default_config = get_default_config())) != 0) { + return NULL; + } + + // mainly used for first time running + if (default_config == NULL) { + return NULL; + } + + file = fopen(default_config, "w"); + if (file == 0) { + return NULL; + } + + int content_length = strlen(DEFAULT_CONFIG_CONTENTS); + _ = fwrite(DEFAULT_CONFIG_CONTENTS, 1, content_length, file); + fclose(file); + if (VERBOSE) { + printf("Creating %s\n", default_config); + } + + char* contents = malloc(content_length * sizeof(char)); + for (int i = 0; i < content_length; i++) { + contents[i] = DEFAULT_CONFIG_CONTENTS[i]; + } + return contents; +} + diff --git a/src/lib.h b/src/lib.h new file mode 100755 index 0000000..999e7d7 --- /dev/null +++ b/src/lib.h @@ -0,0 +1,24 @@ +#include <unistd.h> +#include <string.h> +#include <stdio.h> +#include <sys/stat.h> +#include <dirent.h> +#include <stdio.h> +#include <stdlib.h> + +#define NULL_STR (char*)0 +#define DEFAULT_CONFIG_CONTENTS "^node_modules$\n^target$\n^build$\n^dist$\n^\\.next$\n^\\.output$\n^\\.vercel$\n^\\.wrangler$\n^\\.venv$\n^\\.astro$\n^\\.devenv$\n^__pycache__$" + +typedef enum { + path_type_unknown, + path_type_directory, + path_type_file +} path_type; + +char* get_default_config(); +char* shift(int* argc, char*** argv); +char* get_ignore_file(char* ignore_file, int VERBOSE); +void mkdir_if_not_exist(char* path, int VERBOSE); +void verify_parent_dirs_exist(char* file_path, int VERBOSE); + +void backup(char* target, char* ignore_instructions, int VERBOSE); diff --git a/src/main.c b/src/main.c new file mode 100755 index 0000000..7e6e1ee --- /dev/null +++ b/src/main.c @@ -0,0 +1,123 @@ +#include "lib.h" + +#define VERSION "1.0" + +void help() { + printf("Backup your files\n"); + printf("Usage: Backbencher [OPTIONS] [input]\n"); + printf("Arguments:\n"); + printf("\t[input]... The path of the folder to backup\n"); + printf("Options:\n"); + printf("\t-i --ignore-file <filename> Specify which files to exclude\n\t\t(default = ~/.config/backbencher/.backupignore)\n"); + printf("\t-h --help \tPrint help\n"); + printf("\t-v --verbose \tVerbose logging \n"); + printf("\t-V --version \tPrint version\n"); +} + +void assign(char* to, char* from) { + int i = 0; + for (; from[i] != 0; i++) { + to[i] = from[i]; + } + to[i] = 0; +} + +int source_main(int argc, char** argv) { + shift(&argc, &argv); + + char* ignore_file = get_default_config(); + char* target = NULL_STR; + int VERBOSE = 0; + + while (1) { + char* segment = shift(&argc, &argv); + if (segment == NULL_STR) { + break; + } + else if (segment[0] == '-' && segment[1] == '-') { + if (strcmp(&segment[2], "ignore-file") == 0) { + assign(&ignore_file[0], shift(&argc, &argv)); + if (ignore_file == NULL_STR) { + fprintf(stderr, "Missing <filename> for --ignore-file\n"); + return 1; + } + } + else if (strcmp(&segment[2], "help") == 0) { + help(); + return 0; + } + else if (strcmp(&segment[2], "verbose") == 0) { + VERBOSE = 1; + } + else if (strcmp(&segment[2], "version") == 0) { + printf(VERSION); + printf("\n"); + return 0; + } + } else if (segment[0] == '-') { + if (segment[1] == 'i') { + assign(&ignore_file[0], shift(&argc, &argv)); + if (ignore_file == NULL_STR) { + fprintf(stderr, "Missing <filename> for --ignore-file\n"); + return 1; + } + } + else if (segment[1] == 'h') { + help(); + return 0; + } + else if (segment[1] == 'v') { + VERBOSE = 1; + } + else if (segment[1] == 'V') { + printf(VERSION); + printf("\n"); + return 0; + } + } else { + target = segment; + } + } + + if (target == NULL_STR) { + fprintf(stderr, "Missing [input] - The folder to backup\n"); + return 1; + } + + if (VERBOSE) { + printf("Target folder: %s\n", target); + printf("Using ignore file: %s\n", ignore_file); + printf("Using verbosity: %d\n", VERBOSE); + } + + char* ignore_instructions = get_ignore_file(ignore_file, VERBOSE); + if (ignore_instructions == NULL) { + fprintf(stderr, "I don't have permissions to read the ignore file\n"); + return 1; + } + if (VERBOSE) { + printf("Ignoring rules:\n%s\n", ignore_instructions); + } + + backup(target, ignore_instructions, VERBOSE); + + free(ignore_instructions); + return 0; +} + +int main(int argc, char** argv) { + int result = source_main(argc, argv); + if (result != 0) { + printf("\n"); + help(); + } + return result; +} + +char* shift(int* argc, char*** argv) { + if (argc == 0) return NULL_STR; + --(*argc); + char* output = **argv; + ++(*argv); + return output; +} |
