linkup

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

linkup.c (5820B)


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