linkup

Simple symlink farmer
git clone https://git.sinitax.com/sinitax/linkup
Log | Files | Refs | LICENSE | sfeed.txt

linkup.c (5839B)


      1#include <linux/limits.h>
      2#include <sys/stat.h>
      3#include <fcntl.h>
      4#include <dirent.h>
      5#include <unistd.h>
      6#include <errno.h>
      7#include <string.h>
      8#include <stdarg.h>
      9#include <stdbool.h>
     10#include <stdio.h>
     11#include <stdlib.h>
     12
     13static bool force = false;
     14static bool install = true;
     15static bool nodirs = false;
     16static bool verbose = false;
     17
     18static const char **excluded = NULL;
     19static size_t excludecnt = 0;
     20static size_t excludecap = 0;
     21
     22static void
     23info(const char *fmt, ...)
     24{
     25	va_list ap;
     26
     27	if (!verbose) return;
     28
     29	va_start(ap, fmt);
     30	vfprintf(stderr, fmt, ap);
     31	va_end(ap);
     32}
     33
     34static void
     35die(const char *fmt, ...)
     36{
     37	va_list ap;
     38
     39	va_start(ap, fmt);
     40	fputs("linkup: ", stderr);
     41	vfprintf(stderr, fmt, ap);
     42	if (*fmt && fmt[strlen(fmt)-1] == ':') {
     43		fputc(' ', stderr);
     44		perror(NULL);
     45	} else {
     46		fputc('\n', stderr);
     47	}
     48	va_end(ap);
     49
     50	exit(1);
     51}
     52
     53void
     54normalize(const char *src, char *dst)
     55{
     56	const char *sep, *tok;
     57	char *end;
     58	size_t len;
     59
     60	end = dst;
     61	*end = '\0';
     62
     63	tok = src;
     64	while (tok) {
     65		sep = strchr(tok, '/');
     66		len = sep ? (size_t) (sep - tok) : strlen(tok);
     67		if (len && strncmp(tok, ".", len)) {
     68			if (!strncmp(tok, "..", len)) {
     69				end = strrchr(dst, '/');
     70				if (!end) end = dst;
     71				*end = '\0';
     72			} else {
     73				*end++ = '/';
     74				strncpy(end, tok, len);
     75				end += len;
     76				*end = '\0';
     77			}
     78		}
     79		tok = sep ? sep + 1 : NULL;
     80	}
     81}
     82
     83
     84static void
     85realparent(int dirfd, const char *dirpath, const char *file, char *parent)
     86{
     87	char tmpbuf[PATH_MAX];
     88	ssize_t len;
     89	char *sep;
     90
     91	if ((len = readlinkat(dirfd, file, tmpbuf, PATH_MAX-1)) == -1)
     92		die("readlink '%s/%s':", dirpath, file);
     93	tmpbuf[len] = '\0';
     94
     95	if (tmpbuf[0] != '/') { /* relative path */
     96		strncpy(parent, tmpbuf, PATH_MAX);
     97		snprintf(tmpbuf, PATH_MAX, "%s/%s", dirpath, parent);
     98	}
     99
    100	normalize(tmpbuf, parent);
    101	if ((sep = strrchr(parent, '/')))
    102		*sep = '\0';
    103}
    104
    105static void
    106exclude(const char *arg)
    107{
    108	if (excludecnt == excludecap) {
    109		excludecap = (excludecap + 1) * 2;
    110		excluded = realloc(excluded, excludecap * sizeof(const char *));
    111	}
    112	excluded[excludecnt] = arg;
    113	excludecnt++;
    114}
    115
    116static void
    117do_install(const char *src_path, const char *dst_path)
    118{
    119	char src_fpath[PATH_MAX], dst_fpath[PATH_MAX];
    120	struct dirent *ent;
    121	struct stat attr;
    122	DIR *src_dir, *dst_dir;
    123	int i;
    124
    125	src_dir = opendir(src_path);
    126	if (!src_dir) die("opendir '%s'", src_path);
    127
    128	dst_dir = opendir(dst_path);
    129	if (!dst_dir) die("opendir '%s'", dst_path);
    130
    131	while ((ent = readdir(src_dir))) {
    132		if (!strcmp(ent->d_name, "."))
    133			continue;
    134		if (!strcmp(ent->d_name, ".."))
    135			continue;
    136
    137		for (i = 0; i < excludecnt; i++) {
    138			if (!strcmp(excluded[i], ent->d_name))
    139				break;
    140		}
    141		if (i != excludecnt) {
    142			info("skip %s/%s\n", src_path, ent->d_name);
    143			continue;
    144		}
    145
    146		snprintf(src_fpath, PATH_MAX, "%s/%s", src_path, ent->d_name);
    147		snprintf(dst_fpath, PATH_MAX, "%s/%s", dst_path, ent->d_name);
    148
    149		/* ensure link destination does not exist */
    150		if (!fstatat(dirfd(dst_dir), ent->d_name,
    151				&attr, AT_SYMLINK_NOFOLLOW)) {
    152			if (ent->d_type == DT_DIR && (attr.st_mode & S_IFMT) == S_IFDIR) {
    153				info("skip %s\n", src_fpath);
    154				continue;
    155			}
    156			if (!force && (attr.st_mode & S_IFMT) != S_IFLNK)
    157				die("dst '%s' already exists", dst_fpath);
    158			// TODO: skip if points to src_fpath already
    159			info("rm %s\n", dst_fpath);
    160			if (remove(dst_fpath)) // TODO: removeall
    161				die("remove '%s' failed:", dst_fpath);
    162		} else {
    163			if (nodirs && ent->d_type == DT_DIR) {
    164				info("skip %s\n", src_fpath);
    165				continue;
    166			}
    167		}
    168
    169		info("link %s\n", src_fpath, dst_fpath);
    170		if (symlink(src_fpath, dst_fpath) < 0)
    171			die("linking '%s' failed:", dst_fpath);
    172	}
    173
    174	/* cleanup dangling symlinks */
    175	while ((ent = readdir(dst_dir))) {
    176		if (ent->d_type != DT_LNK)
    177			continue;
    178
    179		/* must point into src */
    180		realparent(dirfd(dst_dir), dst_path, ent->d_name, dst_fpath);
    181		if (strcmp(dst_fpath, src_path))
    182			continue;
    183
    184		/* with missing destination */
    185		if (faccessat(dirfd(dst_dir), ent->d_name, F_OK, 0)) {
    186			info(" unlink %s/%s\n", dst_path, ent->d_name);
    187			if (unlinkat(dirfd(dst_dir), ent->d_name, 0))
    188				die("unlinking '%s/%s' failed:", dst_path, ent->d_name);
    189		}
    190	}
    191
    192	closedir(dst_dir);
    193	closedir(src_dir);
    194}
    195
    196static void
    197do_uninstall(const char *src_path, const char *dst_path)
    198{
    199	struct dirent *ent;
    200	char dst_fpath[PATH_MAX];
    201	DIR *dst_dir;
    202
    203	dst_dir = opendir(dst_path);
    204	if (!dst_dir) die("opendir '%s':", dst_path);
    205
    206	while ((ent = readdir(dst_dir))) {
    207		if (ent->d_type != DT_LNK)
    208			continue;
    209
    210		/* must point into src */
    211		realparent(dirfd(dst_dir), dst_path, ent->d_name, dst_fpath);
    212		if (strcmp(dst_fpath, src_path))
    213			continue;
    214
    215		if (unlinkat(dirfd(dst_dir), ent->d_name, 0))
    216			die("failed to remove '%s/%s':", dst_path, ent->d_name);
    217	}
    218
    219	closedir(dst_dir);
    220}
    221
    222int
    223main(int argc, const char **argv)
    224{
    225	char src_path[PATH_MAX], dst_path[PATH_MAX];
    226	const char *src_arg, *dst_arg;
    227	const char **arg;
    228
    229	src_arg = dst_arg = NULL;
    230	for (arg = &argv[1]; *arg; arg++) {
    231		if (!strcmp(*arg, "-x") || !strcmp(*arg, "--exclude")) {
    232			exclude(*++arg);
    233		} else if (!strcmp(*arg, "-u") || !strcmp(*arg, "--uninstall")) {
    234			install = false;
    235		} else if (!strcmp(*arg, "-f") || !strcmp(*arg, "--force")) {
    236			force = true;
    237		} else if (!strcmp(*arg, "-v") || !strcmp(*arg, "--verbose")) {
    238			verbose = true;
    239		} else if (!strcmp(*arg, "--xdirs")) {
    240			nodirs = true;
    241		} else if (!src_arg) {
    242			src_arg = *arg;
    243		} else if (!dst_arg) {
    244			dst_arg = *arg;
    245		} else {
    246			break;
    247		}
    248	}
    249
    250	if (!src_arg || !dst_arg || *arg) {
    251		fprintf(stderr, "Usage: linkup [-h] [-x FILE..] SRC DST\n");
    252		return 1;
    253	}
    254
    255	if (!realpath(src_arg, src_path))
    256		die("realpath '%s':", src_path);
    257
    258	if (!realpath(dst_arg, dst_path))
    259		die("realpath '%s':", dst_path);
    260
    261	if (install) {
    262		do_install(src_path, dst_path);
    263	} else {
    264		do_uninstall(src_path, dst_path);
    265	}
    266
    267	free(excluded);
    268}