summaryrefslogtreecommitdiff
path: root/src/bulk_ln.c
diff options
context:
space:
mode:
authorhiddenalpha.ch2026-08-30 00:01:23 +0200
committerhiddenalpha.ch2026-08-30 00:01:23 +0200
commit5e97eeceb9dec727fe1bce20660069b5f69a5d8a (patch)
treecc9c5c99408cc16faec6f83e73ad4644bc0a2051 /src/bulk_ln.c
parent48d5db8a3303a862d1a2a89eecfd7c7409c3d4f2 (diff)
downloadbulk-ln-5e97eeceb9dec727fe1bce20660069b5f69a5d8a.tar.gz
bulk-ln-5e97eeceb9dec727fe1bce20660069b5f69a5d8a.zip
de-clutter whole project
Diffstat (limited to 'src/bulk_ln.c')
-rw-r--r--src/bulk_ln.c514
1 files changed, 514 insertions, 0 deletions
diff --git a/src/bulk_ln.c b/src/bulk_ln.c
new file mode 100644
index 0000000..f3cda22
--- /dev/null
+++ b/src/bulk_ln.c
@@ -0,0 +1,514 @@
+#if 0
+--[===========================================================================[
+
+ How to build:
+
+ lua src/bulk_ln.c
+
+ How to use:
+
+ ./bulk-ln --help
+
+ ]===========================================================================]
+function shEsc(s)return "'".. s:gsub("'", "'\"'\"'") .."'" end
+do
+ local thisFile = debug.getinfo(1,"S").source:sub(2)
+ local sh = io.popen("sh", "w")
+ assert(sh, "Failed to launch a shell")
+ sh:write([=[set -e \
+ \
+ && CC="${CC:-gcc}" \
+ && LD="${LD:-gcc}" \
+ && OBJDUMP="${OBJDUMP:-objdump}" \
+ && RIMRAF="${RIMRAF:-rm -rf}" \
+ && BINEXT="${BINEXT:-}" \
+ && CFLAGS="${CFLAGS:- -std=c99 -Wall -Werror -pedantic -nostdlib -nodefaultlibs}" \
+ && LDFLAGS="${LDFLAGS:-}" \
+ && TMPDIR="${TMPDIR:-/tmp}" \
+ && OUTFILE="bulk-ln${BINEXT?}" \
+ \
+ && if test -z "$PROJECT_VERSION" ;then true \
+ && tag="$(git tag|grep -E '^v'|sed -E 's,^v,,'|sort -V|tail -n1)" \
+ && cnt="$(git rev-list --count "v${tag:?}"..HEAD)" \
+ && hsh="$(git rev-parse --short HEAD)" \
+ && if test -n "$tag" -a -n "$cnt" -a -n "$hsh" ;then true \
+ && PROJECT_VERSION="${tag:?}-${cnt:?}-g${hsh:?}" \
+ ;else true \
+ && PROJECT_VERSION="$(date -u +0.0.0-%Y%m%d.%H%M%S)" \
+ ;fi \
+ ;fi \
+ \
+ && ${CC:?} -c -o "${TMPDIR:?}"/YC71NcBIJMwpwUfL ]=].. shEsc(thisFile) ..[=[ ${CFLAGS?} -DPROJECT_VERSION="${PROJECT_VERSION:?}" -DEXPOSE_MAIN=1 \
+ && ${LD:?} -o ./"${OUTFILE:?}" "${TMPDIR:?}"/YC71NcBIJMwpwUfL ${LDFLAGS?} \
+ && ${RIMRAF:?} "${TMPDIR:?}"/YC71NcBIJMwpwUfL \
+ && bullshit=$(${OBJDUMP?} -p "${OUTFILE:?}"|grep DLL\ Name|egrep -v ' (KERNEL32.dll|SHELL32.dll|WS2_32.dll|ADVAPI32.dll|msvcrt.dll|GDI32.dll|USER32.dll)$'||true) \
+ && if test -n "$bullshit"; then printf '\n ERROR: Bullshit has sneaked in:\n\n%s\n\n' "$bullshit"; rm "${OUTFILE:?}"; false; fi \
+]=])
+ -- Shitty systems maybe need adaptions like:
+ -- && LDFLAGS="-Wl,-lws2_32,-l:libwinpthread.a" \
+ local ok, how, code = sh:close()
+ if not ok then os.exit(code) end
+end
+--[===========================================================================[
+#endif
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#define STR_QUOT_IAHGEWIH(s) #s
+#define STR_QUOT(s) STR_QUOT_IAHGEWIH(s)
+
+#ifndef PRIVATE
+# define PRIVATE(T) static T
+#endif
+
+#ifndef DATA_FILE_FIELD_SEP_CHR
+# define DATA_FILE_FIELD_SEP_CHR '\t'
+#endif
+
+
+typedef struct BulkLn {
+ /* Root context
+ */
+
+ char *dataFilePath;
+ /* File (path) where we read the paths-to-link from. The special
+ * value - (dash) means to use the stdin stream.
+ */
+
+ FILE *dataFd;
+
+ int createdLinksCount;
+ /* Count of links we created
+ */
+
+ int createdDirsCount;
+ /* Count of direcrories we created
+ */
+
+ int dryRun;
+ /* If a dry-run got requested
+ */
+
+ int isPrintEachCreateLink;
+ /* If true, we will print to stdout for every link we create (like we do
+ * in dry run)
+ 0*/
+
+ int isPrintEachMkdir;
+ /* print every call we make to mkdir to stdout. Usually only useful for
+ * debugging
+ */
+
+ int isPrintStatus;
+ /* if true, we periodically print some kind of progress to stderr
+ */
+
+ int isPrintSummary;
+ /* if true, we print a summary what we did at the end of the run to stderr
+ */
+
+ int isRelinkExistingFiles;
+ /* if true, we will override existing files. This will be enabled for
+ * example if --force got specified.
+ */
+} BulkLn;
+
+
+
+PRIVATE(void)
+printHelp(){
+ printf(" \n %s @ " STR_QUOT(PROJECT_VERSION) "\n"
+ " \n"
+ " Takes paths (pairwise) from stdin (see --stdin for details) and\n"
+ " creates a hardlink for each pair from the 1st path to the 2nd.\n"
+ " \n"
+ " Writing a custom implementation of 'ln' got necessary as we found no\n"
+ " way to instruct 'ln' to create a few thousand links in an acceptable\n"
+ " amount of time. Remind that wheels that do not fit should be\n"
+ " re-invented (yes, no matter how much they already exist!). So now we\n"
+ " have it. Our re-invented wheel that finally fits our use-case ;) \n"
+ " \n"
+ " See also\n"
+ " https://pubs.opengroup.org/onlinepubs/009695399/utilities/ln.html\n"
+ " for info about the original 'ln'.\n"
+ " \n"
+ " Options:\n"
+ " \n"
+ " --stdin\n"
+ " Read the path pairs to link from stdin. The format is like:\n"
+ " \n"
+ " <src-path> <tab> <dst-path> <newline>\n"
+ " \n"
+ " Example:\n"
+ " \n"
+ " origin/foo.txt\tnew/gugg.txt\n"
+ " origin/bar.txt\tnew/da.txt\n"
+ " \n"
+ " HINT: Preferred <newline> is LF. But CRLF should work too.\n"
+ " \n"
+ " --quiet\n"
+ " Don't print status or similar stuff. Errors will still be\n"
+ " printed to stderr.\n"
+ " \n"
+ " --verbose\n"
+ " Print stupid amount of logs. Usually only helpful for debugging.\n"
+ " Should NOT be combined with --quiet as this would be nonsense\n"
+ " anyway.\n"
+ " \n"
+ " --dry-run\n"
+ " Will print the actions to stdout instead executing them.\n"
+ " HINT: Directory count in summary will be inaccurate in this\n"
+ " mode.\n"
+ " \n"
+ " --force\n"
+ " Same meaning as in original 'ln' command.\n"
+ " \n", strrchr(__FILE__,'/')+1 );
+}
+
+
+/** returns non-zero on errors. Error messages will already be printed
+ * internally. */
+PRIVATE(int)
+parseArgs( int argc, char**argv, BulkLn*bulkLn ){
+ /* init (aka set defaults) */
+ bulkLn->dataFilePath = NULL;
+ bulkLn->dryRun = 0;
+ bulkLn->isPrintEachCreateLink = 0;
+ bulkLn->isPrintEachMkdir = 0;
+ bulkLn->isPrintStatus = !0;
+ bulkLn->isPrintSummary = !0;
+ bulkLn->isRelinkExistingFiles = 0;
+
+ /* Parse args */
+ for( int i = 1 ; i < argc ; ++i ){
+ char *arg = argv[i];
+ if( !strcmp(arg, "--help") ){
+ printHelp();
+ return -1;
+ }
+ else if( !strcmp(arg, "--dry-run") ){
+ bulkLn->dryRun = !0;
+ }
+ else if( !strcmp(arg, "--force") ){
+ bulkLn->isRelinkExistingFiles = !0;
+ }
+ else if( !strcmp(arg, "--quiet") ){
+ bulkLn->isPrintStatus = 0;
+ bulkLn->isPrintSummary = 0;
+ }
+ else if( !strcmp(arg, "--stdin") ){
+ bulkLn->dataFilePath = "-";
+ }
+ else if( !strcmp(arg, "--verbose") ){
+ bulkLn->isPrintStatus = !0;
+ bulkLn->isPrintSummary = !0;
+ bulkLn->isPrintEachCreateLink = !0;
+ bulkLn->isPrintEachMkdir = !0;
+ }
+ else{
+ fprintf(stderr, "EINVAL: '%s'\n", arg);
+ return -1;
+ }
+ }
+
+ /* MUST specify input method. Yes there is only one input method. But
+ * requiring args is the simplest way to prevent damage in case someone (eg
+ * accidentally) invokes the utility wihout args. Further this also makes
+ * the utility easier to extend wihout breaking everything. */
+ if( bulkLn->dataFilePath == NULL ){
+ fprintf(stderr, "EINVAL: '--stdin' missing.\n");
+ return -1;
+ }
+
+ return 0;
+}
+
+
+/**
+ * Like: mkdirs -p path
+ *
+ * WARN: Passed 'path' might be temporarily changed during execution.
+ * Nevertheless will be in original state after return.
+ */
+PRIVATE(int)
+mkdirs( char*path, BulkLn*bulkLn ){
+ int err;
+ char *tmpEnd = path;
+ /* Backup original length so we later still can recognize the original end
+ * as we are going to place zeroes in the path during the run. */
+ char *pathEnd = path + strlen(path);
+
+ for(;;){
+ /* Loop for each slash in the path beginning from the topmost dir toward the
+ * innermost dir.
+ * This way our path gets longer by one segment each time we call 'mkdir' below.
+ * This is to have the same effect as we would call 'mkdir --parents' from cli.
+ * In other words we make sure every parent dir exists before creating the next
+ * one */
+
+ tmpEnd = strchr(tmpEnd + 1, '/');
+ if( tmpEnd == NULL ){
+ /* The last (innermost) segment to create a dir for */
+ tmpEnd = pathEnd;
+ }
+
+ /* Temporarily zero-terminate the path so we can create the parent dir
+ * up to there */
+ tmpEnd[0] = '\0';
+
+ /* Print if requested */
+ if( bulkLn->dryRun || bulkLn->isPrintEachMkdir ){
+ printf("mkdir(\"%s\")\n", path);
+ }
+
+ /* Create dir up to that found path */
+ if( ! bulkLn->dryRun ){
+ /* Perform the real action */
+ /* mode gets masked by umask and after that we have the defaults
+ * (aka what we want) */
+ err = mkdir(path, 0777);
+ if( err ){
+ if( errno == EEXIST ){
+ /* Fine :) So just continue with the next one. */
+ }else{
+ fprintf(stderr, "mkdir(\"%s\"): %s\n", path, strerror(errno));
+ err = -1; goto resolveWithErr;
+ }
+ }else{
+ /* Only increment the directory counter if we really created
+ * the dir. Eg if it did NOT already exist */
+ bulkLn->createdDirsCount += 1;
+ }
+ }else{
+ /* We cannot really count how many dirs we created. Because maybe
+ * we are (hypothetically) creating this directory the 100th time
+ * now. But we have no way to tell as we do not really create em.
+ * So just increment to have some value at all */
+ bulkLn->createdDirsCount += 1;
+ }
+
+ if( tmpEnd == pathEnd ){
+ /* Nothing to restore as we point to end-of-string.
+ * This also means we're done. So end the loop */
+ break;
+ }else{
+ /* Restore where we did cut-off the path */
+ tmpEnd[0] = '/';
+ /* Then loop to add and process one more segment */
+ }
+ }
+
+ err = 0;
+resolveWithErr:
+ return err;
+}
+
+
+/** Like: ln srcPath dstPath */
+PRIVATE(int)
+createHardlink( char*srcPath, char*dstPath, BulkLn*bulkLn ){
+ int err;
+
+ if( bulkLn->dryRun || bulkLn->isPrintEachCreateLink ){
+ printf("link(\"%s\", \"%s\")\n", srcPath, dstPath);
+ }
+
+ if( ! bulkLn->dryRun ){
+ /* Perform the real action */
+ if( bulkLn->isRelinkExistingFiles ){
+ /* Delete beforehand so we can be sure 'link' does not fail due
+ * already existing file */
+ err = unlink(dstPath);
+ if( err ){
+ if( errno == ENOENT ){
+ /* There is no such entry we could delete. So we're already
+ * fine :) */
+ }else{
+ /* Some other (unepxected) error */
+ fprintf(stderr, "unlink(%s): %s\n", dstPath, strerror(errno));
+ err = -1; goto resolveWithErr;
+ }
+ }
+ }
+ err = link(srcPath, dstPath);
+ if( err ){
+ fprintf(stderr, "link(\"%s\", \"%s\"): %s\n", srcPath, dstPath, strerror(errno));
+ err = -1; goto resolveWithErr;
+ }
+ }
+
+ bulkLn->createdLinksCount += 1;
+
+ err = 0;
+resolveWithErr:
+ return err;
+}
+
+
+PRIVATE(int)
+onPathPair( char*srcPath, char*dstPath, BulkLn*bulkLn ){
+ assert(srcPath != NULL);
+ assert(dstPath != NULL);
+ assert(bulkLn != NULL);
+ int err;
+
+ if( bulkLn->isPrintStatus && bulkLn->createdLinksCount % 10000 == 0 ){
+ fprintf(stderr, "Created %7d links so far.\n", bulkLn->createdLinksCount);
+ }
+
+ /* Search end of parent dir path */
+ char *tmpEnd = strrchr(dstPath, '/');
+ if( tmpEnd != NULL ){
+ /* Temporarily cut-off the last segment (filename) to create the
+ * parent-dirs */
+ tmpEnd[0] = '\0';
+ /* Create missing parent dirs */
+ err = mkdirs(dstPath, bulkLn);
+ /* Restore path */
+ tmpEnd[0] = '/';
+ if( err ){ err = -1; goto resolveWithErr; }
+ }
+
+ err = createHardlink(srcPath, dstPath, bulkLn);
+ if( err ){ err = -1; goto resolveWithErr; }
+
+ err = 0;
+resolveWithErr:
+ return err;
+}
+
+
+PRIVATE(int)
+parseDataFileAsPairPerLine( BulkLn*bulkLn ){
+ int err;
+ size_t buf_cap = 0;
+ size_t buf_len = 0;
+ char *buf = NULL;
+ size_t lineNum = 0;
+
+ for(;;){
+ lineNum += 1;
+
+ /* Read input line-by-line. Not the most elegant way to parse stuff,
+ * but should suffice for our use-case */
+ err = getline(&buf, &buf_cap, bulkLn->dataFd);
+ if( err < 0 ){
+ /* Error handling */
+ if( feof(bulkLn->dataFd) ){
+ break; /* End-Of-File. Just break off the loop */
+ }else if( ferror(bulkLn->dataFd) ){
+ fprintf(stderr, "getline(%s): %s\n", bulkLn->dataFilePath, strerror(errno));
+ err = -1; goto resolveWithErr;
+ }else{
+ abort(); /* I don't know how this could happen */
+ }
+ }
+ buf_len = err;
+
+ /* Extract the two paths from our line */
+ char *srcPath = buf;
+ char *tab = memchr(buf, DATA_FILE_FIELD_SEP_CHR, buf_len);
+ if( tab == NULL ){
+ fprintf(stderr, "Too few field separators (tab) in '%s' @ %lu\n",
+ bulkLn->dataFilePath, lineNum);
+ err = -1; goto resolveWithErr;
+ }
+ char *unwantedTab = memchr(tab + 1, DATA_FILE_FIELD_SEP_CHR, buf_len - (tab + 1 - buf));
+ if( unwantedTab != NULL ){
+ fprintf(stderr, "Too many field separators (tab) in '%s' line %lu\n",
+ bulkLn->dataFilePath, lineNum);
+ err = -1; goto resolveWithErr;
+ }
+ char *dstPath = tab + 1; /* <- path starts one char after the separator */
+ char *dstPath_end = buf + buf_len;
+ for(;; --dstPath_end ){
+ if( dstPath_end < buf ){
+ fprintf(stderr, "IMHO cannot happen %s:%d\n", __FILE__, __LINE__);
+ err = -1; goto resolveWithErr;
+ }
+ if( dstPath_end[0]=='\n' || dstPath_end[0]=='\0' || dstPath_end[0]=='\r' ){
+ continue; /* last char not found yet */
+ }
+ /* 'dstPath_end' now points to the last char of our line. So add
+ * one to point to the 'end' */
+ dstPath_end += 1;
+ break;
+ }
+
+ /* Zero-Terminate the two strings */
+ tab[0] = '\0';
+ dstPath_end[0] = '\0';
+
+ /* Publish this pair for processing */
+ err = onPathPair(srcPath, dstPath, bulkLn);
+ if( err ){ err = -1; goto resolveWithErr; }
+ }
+
+ if( bulkLn->isPrintStatus ){
+ fprintf(stderr, "Parsed %lu records from '%s'\n", lineNum - 1, bulkLn->dataFilePath);
+ }
+
+ err = 0;
+resolveWithErr:
+ free(buf);
+ return err;
+}
+
+
+int
+bulk_ln_main( int argc, char**argv ){
+ int err;
+ BulkLn bulkLn = {0};
+ #define bulkLn (&bulkLn)
+
+ err = parseArgs(argc, argv, bulkLn);
+ if( err ){ err = -1; goto resolveWithErr; }
+
+ /* Open data source */
+ if( !strcmp(bulkLn->dataFilePath, "-") ){
+ bulkLn->dataFd = stdin;
+ }else{
+ bulkLn->dataFd = fopen(bulkLn->dataFilePath, "rb");
+ if( bulkLn->dataFd == NULL ){
+ fprintf(stderr, "fopen(%s): %s\n", bulkLn->dataFilePath, strerror(errno));
+ err = -1; goto resolveWithErr;
+ }
+ }
+
+ err = parseDataFileAsPairPerLine(bulkLn);
+ if( err ){ err = -1; goto resolveWithErr; }
+
+ if( bulkLn->isPrintSummary ){
+ fprintf(stderr, "Created %d directories and linked %d files.\n",
+ bulkLn->createdDirsCount, bulkLn->createdLinksCount);
+ }
+
+ err = 0;
+resolveWithErr:
+ if( bulkLn->dataFd != NULL && bulkLn->dataFd != stdin ){
+ fclose(bulkLn->dataFd);
+ }
+ return err;
+ #undef bulkLn
+}
+
+
+#if EXPOSE_MAIN
+int
+main( int argc, char**argv ){
+ argc = bulk_ln_main(argc, argv);
+ if( argc < 0 ) argc = -argc;
+ return (argc >= 0 && argc <= 0x7F) ? argc : 1;
+}
+#endif
+
+
+/*]===========================================================================]--*/