commit a286b18428747eafbb6923b86fe5f25c3f70f7bf
Author: Louis Burda <quent.burda@gmail.com>
Date: Thu, 20 Oct 2022 01:16:22 +0200
Initial prototype
Diffstat:
A | .gitignore | | | 1 | + |
A | Makefile | | | 14 | ++++++++++++++ |
A | linkup.c | | | 153 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
3 files changed, 168 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+linkup
diff --git a/Makefile b/Makefile
@@ -0,0 +1,14 @@
+all: linkup
+
+clean:
+ rm -f linkup
+
+linkup: linkup.c
+
+install: linkup
+ install -m755 linkup -T "$(DESTDIR)$(PREFIX)$(BINDIR)/linkup"
+
+uninstall:
+ rm -f "$(DESTDIR)$(PREFIX)$(BINDIR)/linkup"
+
+.PHONY: all clean install uninstall
diff --git a/linkup.c b/linkup.c
@@ -0,0 +1,153 @@
+#include <linux/limits.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <err.h>
+#include <errno.h>
+#include <string.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+static const char usage[] = "linkup [-h] [-x FILE..] SOURCE DEST";
+
+static bool force;
+
+bool
+exists(int dirfd, const char *path)
+{
+ struct stat attr;
+
+ return !fstatat(dirfd, path, &attr, AT_SYMLINK_NOFOLLOW);
+}
+
+void
+removeall(int pfd, const char *path)
+{
+ struct dirent *ent;
+ struct stat attr;
+ DIR *dir;
+ int fd;
+
+ if (fstatat(pfd, path, &attr, AT_SYMLINK_NOFOLLOW) < 0) {
+ if (errno == ENOENT) return;
+ err(1, "fstatat");
+ }
+
+ if ((attr.st_mode & S_IFMT) == S_IFDIR) {
+ fd = openat(pfd, path, 0);
+ dir = fdopendir(fd);
+ if (!dir) err(1, "opendir");
+ while ((ent = readdir(dir)))
+ removeall(fd, ent->d_name);
+ closedir(dir);
+ }
+
+ unlinkat(pfd, path, AT_REMOVEDIR);
+}
+
+int
+main(int argc, const char **argv)
+{
+ char spbuf[PATH_MAX], dpbuf[PATH_MAX];
+ char spath[PATH_MAX], dpath[PATH_MAX];
+ struct dirent *ent;
+ struct stat attr;
+ const char *sarg, *darg;
+ const char **arg;
+ DIR *sdir, *ddir;
+ const char **ignored;
+ int ignorecnt;
+ int ignorecap;
+ int i;
+
+ ignored = NULL;
+ ignorecap = 0;
+ ignorecnt = 0;
+
+ force = false;
+ sarg = darg = NULL;
+ for (arg = &argv[1]; *arg; arg++) {
+ if (!strcmp(*arg, "-x")) {
+ if (ignorecnt == ignorecap) {
+ ignorecap = (ignorecap + 1) * 2;
+ ignored = realloc(ignored,
+ ignorecap * sizeof(const char *));
+ }
+ ignored[ignorecnt] = *arg;
+ ignorecnt++;
+ } else if (!strcmp(*arg, "-f")) {
+ force = true;
+ } else if (!sarg) {
+ sarg = *arg;
+ } else if (!darg) {
+ darg = *arg;
+ } else {
+ printf("Usage: %s\n", usage);
+ return 0;
+ }
+ }
+
+ if (!sarg || !darg) {
+ fprintf(stderr, "Usage: %s\n", usage);
+ return 1;
+ }
+
+ if (!realpath(sarg, spath))
+ err(1, "realpath src");
+
+ if (!realpath(darg, dpath))
+ err(1, "realpath dst");
+
+ sdir = opendir(spath);
+ if (!sdir) err(1, "opendir src");
+
+ ddir = opendir(dpath);
+ if (!ddir) err(1, "opendir dst");
+
+ while ((ent = readdir(sdir))) {
+ /* skip ignored files */
+ for (i = 0; i < ignorecnt; i++) {
+ if (!strcmp(ignored[i], ent->d_name))
+ break;
+ }
+ if (i != ignorecnt) continue;
+
+ /* skip default directories */
+ if (!strcmp(ent->d_name, "."))
+ continue;
+ if (!strcmp(ent->d_name, ".."))
+ continue;
+
+ /* ensure link destination does not exist */
+ if (!fstatat(dirfd(ddir), ent->d_name,
+ &attr, AT_SYMLINK_NOFOLLOW)) {
+ if ((attr.st_mode & S_IFMT) != S_IFLNK && !force)
+ errx(1, "dst %s already exists\n", ent->d_name);
+ }
+ removeall(dirfd(ddir), ent->d_name);
+
+ /* create link */
+ snprintf(spbuf, PATH_MAX, "%s/%s", spath, ent->d_name);
+ snprintf(dpbuf, PATH_MAX, "%s/%s", dpath, ent->d_name);
+ if (symlink(spbuf, dpbuf) < 0)
+ err(1, "link");
+ }
+
+ /* clean dangling symlinks */
+ while ((ent = readdir(ddir))) {
+ if (ent->d_type != DT_LNK)
+ continue;
+ snprintf(dpbuf, PATH_MAX, "%s/%s", dpath, ent->d_name);
+ if (readlink(dpbuf, dpbuf, PATH_MAX) < 0)
+ err(1, "readlink");
+ realpath(dpbuf, dpbuf);
+ if (strncmp(dpbuf, spath, strlen(spath)))
+ break;
+ if (fstatat(dirfd(ddir), ent->d_name, &attr, 0) < 0)
+ unlinkat(dirfd(ddir), ent->d_name, 0);
+ }
+
+ free(ignored);
+}