summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAargh Rai <shubh2007rai@gmail.com>2026-03-28 19:03:45 +0530
committerAargh Rai <shubh2007rai@gmail.com>2026-03-28 19:03:45 +0530
commitc7194b9b07a9c80344b52e002d0863fd258f0b38 (patch)
tree258e0aae8fdbdce64d8df36e75138a186358cd6b
basic backup system, which utlizies regex for ignore files
-rwxr-xr-x.envrc12
-rwxr-xr-x.gitignore12
-rwxr-xr-xCMakeLists.txt14
-rwxr-xr-xLICENSE21
-rwxr-xr-xREADME.md12
-rwxr-xr-xdevenv.lock103
-rwxr-xr-xdevenv.nix8
-rwxr-xr-xdevenv.yaml15
-rw-r--r--src/backup.c172
-rw-r--r--src/config.c24
-rw-r--r--src/ignore.c66
-rwxr-xr-xsrc/lib.h24
-rwxr-xr-xsrc/main.c123
13 files changed, 606 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100755
index 0000000..cc5c18b
--- /dev/null
+++ b/.envrc
@@ -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)
diff --git a/LICENSE b/LICENSE
new file mode 100755
index 0000000..1b40a2e
--- /dev/null
+++ b/LICENSE
@@ -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;
+}