/ src / clean.c
clean.c
  1  /*-
  2   * Copyright (c) 2011-2012 Julien Laffaye <jlaffaye@FreeBSD.org>
  3   * Copyright (c) 2013-2014 Matthew Seaman <matthew@FreeBSD.org>
  4   * Copyright (c) 2014 Vsevolod Stakhov <vsevolod@FreeBSD.org>
  5   * Copyright (c) 2016-2025 Baptiste Daroussin <bapt@FreeBSD.org>
  6   *
  7   * SPDX-License-Identifier: BSD-2-Clause
  8   */
  9  
 10  #ifdef HAVE_CONFIG_H
 11  #include "pkg_config.h"
 12  #endif
 13  
 14  #include <sys/stat.h>
 15  /* For MIN */
 16  #include <sys/param.h>
 17  
 18  #if __has_include(<sys/capsicum.h>)
 19  #define HAVE_CAPSICUM 1
 20  #include <sys/capsicum.h>
 21  #endif
 22  
 23  #include <assert.h>
 24  #include <err.h>
 25  #include <getopt.h>
 26  #if __has_include(<libutil.h>)
 27  #include <libutil.h>
 28  #endif
 29  #include <pkg.h>
 30  #include <stdbool.h>
 31  #include <string.h>
 32  #include <unistd.h>
 33  #include <fcntl.h>
 34  #include <dirent.h>
 35  #include <errno.h>
 36  
 37  #include <bsd_compat.h>
 38  
 39  #include "pkgcli.h"
 40  #include "pkghash.h"
 41  #include "xmalloc.h"
 42  #include "pkg/vec.h"
 43  
 44  #define OUT_OF_DATE	(1U<<0)
 45  #define REMOVED		(1U<<1)
 46  #define CKSUM_MISMATCH	(1U<<2)
 47  #define SIZE_MISMATCH	(1U<<3)
 48  #define ALL		(1U<<4)
 49  
 50  static size_t
 51  add_to_dellist(int fd, charv_t *dl, const char *cachedir, const char *path)
 52  {
 53  	static bool first_entry = true;
 54  	struct stat st;
 55  	char *store_path;
 56  	const char *relpath;
 57  	size_t sz = 0;
 58  
 59  	assert(path != NULL);
 60  
 61  	store_path = xstrdup(path);
 62  
 63  	if (!quiet) {
 64  		if (first_entry) {
 65  			first_entry = false;
 66  			printf("The following package files will be deleted:"
 67  			    "\n");
 68  		}
 69  		printf("\t%s\n", store_path);
 70  	}
 71  
 72  	relpath = path + strlen(cachedir) + 1;
 73  	if (fstatat(fd, relpath, &st, AT_SYMLINK_NOFOLLOW) != -1 && S_ISREG(st.st_mode))
 74  		sz = st.st_size;
 75  	vec_push(dl, store_path);
 76  
 77  	return (sz);
 78  }
 79  
 80  static int
 81  delete_dellist(int fd, const char *cachedir,  charv_t *dl)
 82  {
 83  	struct stat st;
 84  	int retcode = EXIT_SUCCESS;
 85  	int flag = 0;
 86  	unsigned int count = 0, processed = 0;
 87  	char *file, *relpath;
 88  
 89  	count = dl->len;
 90  	progressbar_start("Deleting files");
 91  	vec_foreach(*dl, i) {
 92  		flag = 0;
 93  		relpath = file = dl->d[i];
 94  		relpath += strlen(cachedir) + 1;
 95  		if (fstatat(fd, relpath, &st, AT_SYMLINK_NOFOLLOW) == -1) {
 96  			++processed;
 97  			progressbar_tick(processed, dl->len);
 98  			warn("can't stat %s", file);
 99  			continue;
100  		}
101  		if (S_ISDIR(st.st_mode))
102  			flag = AT_REMOVEDIR;
103  		if (unlinkat(fd, relpath, flag) == -1) {
104  			warn("unlink(%s)", file);
105  			retcode = EXIT_FAILURE;
106  		}
107  		free(file);
108  		dl->d[i] = NULL;
109  		++processed;
110  		progressbar_tick(processed, dl->len);
111  	}
112  	progressbar_tick(processed, dl->len);
113  
114  	if (!quiet) {
115  		if (retcode != EXIT_SUCCESS)
116  			printf("%d package%s could not be deleted\n",
117  			      count, count > 1 ? "s" : "");
118  	}
119  	return (retcode);
120  }
121  
122  static pkghash *
123  populate_sums(struct pkgdb *db)
124  {
125  	struct pkg *p = NULL;
126  	struct pkgdb_it *it = NULL;
127  	const char *sum;
128  	char *cksum;
129  	size_t slen;
130  	pkghash *suml = NULL;
131  
132  	suml = pkghash_new();
133  	it = pkgdb_repo_search(db, "*", MATCH_GLOB, FIELD_NAME, FIELD_NONE, NULL);
134  	while (pkgdb_it_next(it, &p, PKG_LOAD_BASIC) == EPKG_OK) {
135  		pkg_get(p, PKG_ATTR_CKSUM, &sum);
136  		slen = MIN(strlen(sum), PKG_FILE_CKSUM_CHARS);
137  		cksum = strndup(sum, slen);
138  		pkghash_safe_add(suml, cksum, NULL, NULL);
139  		free(cksum);
140  	}
141  	pkgdb_it_free(it);
142  
143  	return (suml);
144  }
145  
146  /*
147   * Extract hash from filename in format <name>-<version>~<hash>.txz
148   */
149  static bool
150  extract_filename_sum(const char *fname, char sum[])
151  {
152  	const char *tilde_pos, *dot_pos;
153  
154  	dot_pos = strrchr(fname, '.');
155  	if (dot_pos == NULL)
156  		dot_pos = fname + strlen(fname);
157  
158  	tilde_pos = strrchr(fname, '~');
159  	/* XXX Legacy fallback; remove eventually. */
160  	if (tilde_pos == NULL)
161  		tilde_pos = strrchr(fname, '-');
162  	if (tilde_pos == NULL)
163  		return (false);
164  	else if (dot_pos < tilde_pos)
165  		dot_pos = fname + strlen(fname);
166  
167  	if (dot_pos - tilde_pos != PKG_FILE_CKSUM_CHARS + 1)
168  		return (false);
169  
170  	strlcpy(sum, tilde_pos + 1, PKG_FILE_CKSUM_CHARS + 1);
171  	return (true);
172  }
173  
174  static int
175  recursive_analysis(int fd, struct pkgdb *db, const char *dir,
176      const char *cachedir, charv_t *dl, pkghash **sumlist, bool all,
177      size_t *total)
178  {
179  	DIR *d;
180  	struct dirent *ent;
181  	int newfd, tmpfd;
182  	char path[MAXPATHLEN], csum[PKG_FILE_CKSUM_CHARS + 1],
183  		link_buf[MAXPATHLEN];
184  	const char *name;
185  	ssize_t link_len;
186  	size_t nbfiles = 0, added = 0;
187  	pkghash_entry *e;
188  
189  	tmpfd = dup(fd);
190  	d = fdopendir(tmpfd);
191  	if (d == NULL) {
192  		close(tmpfd);
193  		warnx("Unable to open the directory %s", dir);
194  		return (0);
195  	}
196  
197  	while ((ent = readdir(d)) != NULL) {
198  		if (STREQ(ent->d_name, ".") ||
199  		    STREQ(ent->d_name, ".."))
200  			continue;
201  		snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name);
202  		if (ent->d_type == DT_DIR) {
203  			nbfiles++;
204  			newfd = openat(fd, ent->d_name, O_DIRECTORY|O_CLOEXEC, 0);
205  			if (newfd == -1) {
206  				warnx("Unable to open the directory %s",
207  				    path);
208  				continue;
209  			}
210  			if (recursive_analysis(newfd, db, path, cachedir, dl,
211  			    sumlist, all, total) == 0 || all) {
212  				add_to_dellist(fd, dl, cachedir, path);
213  				added++;
214  			}
215  			close(newfd);
216  			continue;
217  		}
218  		if (ent->d_type != DT_LNK && ent->d_type != DT_REG)
219  			continue;
220  		nbfiles++;
221  		if (all) {
222  			*total += add_to_dellist(fd, dl, cachedir, path);
223  			continue;
224  		}
225  		if (*sumlist == NULL) {
226  			*sumlist = populate_sums(db);
227  		}
228  		name = ent->d_name;
229  		if (ent->d_type == DT_LNK) {
230  			/* Dereference the symlink and check it for being
231  			 * recognized checksum file, or delete the symlink
232  			 * later. */
233  			if ((link_len = readlinkat(fd, ent->d_name, link_buf,
234  			    sizeof(link_buf))) == -1)
235  				continue;
236  			link_buf[link_len] = '\0';
237  			name = link_buf;
238  		}
239  
240  		e = NULL;
241  		if (extract_filename_sum(name, csum)) {
242  			e = pkghash_get(*sumlist, csum);
243  		}
244  		if (e == NULL) {
245  			added++;
246  			*total += add_to_dellist(fd, dl, cachedir, path);
247  		}
248  	}
249  	closedir(d);
250  	return (nbfiles - added);
251  }
252  
253  void
254  usage_clean(void)
255  {
256  	fprintf(stderr, "Usage: pkg clean [-anqy]\n\n");
257  	fprintf(stderr, "For more information see 'pkg help clean'.\n");
258  }
259  
260  int
261  exec_clean(int argc, char **argv)
262  {
263  	struct pkgdb	*db = NULL;
264  	pkghash		*sumlist = NULL;
265  	charv_t		 dl = vec_init();
266  	const char	*cachedir;
267  	bool		 all = false;
268  	int		 retcode;
269  	int		 ch;
270  	int		 cachefd = -1;
271  	size_t		 total = 0;
272  	char		 size[8];
273  #ifdef HAVE_CAPSICUM
274  	cap_rights_t rights;
275  #endif
276  
277  	struct option longopts[] = {
278  		{ "all",	no_argument,	NULL,	'a' },
279  		{ "dry-run",	no_argument,	NULL,	'n' },
280  		{ "quiet",	no_argument,	NULL,	'q' },
281  		{ "yes",	no_argument,	NULL,	'y' },
282  		{ NULL,		0,		NULL,	0   },
283  	};
284  
285  	while ((ch = getopt_long(argc, argv, "+anqy", longopts, NULL)) != -1) {
286  		switch (ch) {
287  		case 'a':
288  			all = true;
289  			break;
290  		case 'n':
291  			dry_run = true;
292  			break;
293  		case 'q':
294  			quiet = true;
295  			break;
296  		case 'y':
297  			yes = true;
298  			break;
299  		default:
300  			usage_clean();
301  			return (EXIT_FAILURE);
302  		}
303  	}
304  
305  	cachedir = pkg_get_cachedir();
306  	cachefd = pkg_get_cachedirfd();
307  	if (cachefd == -1) {
308  		warn("Unable to open %s", cachedir);
309  		return (errno == ENOENT ? EXIT_SUCCESS : EXIT_FAILURE);
310  	}
311  
312  	retcode = pkgdb_access(PKGDB_MODE_READ, PKGDB_DB_REPO);
313  
314  	if (retcode == EPKG_ENOACCESS) {
315  		warnx("Insufficient privileges to clean old packages");
316  		close(cachefd);
317  		return (EXIT_FAILURE);
318  	} else if (retcode == EPKG_ENODB) {
319  		warnx("No package database installed.  Nothing to do!");
320  		close(cachefd);
321  		return (EXIT_SUCCESS);
322  	} else if (retcode != EPKG_OK) {
323  		warnx("Error accessing the package database");
324  		close(cachefd);
325  		return (EXIT_FAILURE);
326  	}
327  
328  	retcode = EXIT_FAILURE;
329  
330  	if (pkgdb_open(&db, PKGDB_REMOTE) != EPKG_OK) {
331  		close(cachefd);
332  		return (EXIT_FAILURE);
333  	}
334  
335  	if (pkgdb_obtain_lock(db, PKGDB_LOCK_READONLY) != EPKG_OK) {
336  		pkgdb_close(db);
337  		close(cachefd);
338  		warnx("Cannot get a read lock on a database, it is locked by "
339  		    "another process");
340  		return (EXIT_FAILURE);
341  	}
342  
343  #ifdef HAVE_CAPSICUM
344  		cap_rights_init(&rights, CAP_READ, CAP_LOOKUP, CAP_FSTATFS,
345  		    CAP_FSTAT, CAP_UNLINKAT);
346  		if (cap_rights_limit(cachefd, &rights) < 0 && errno != ENOSYS ) {
347  			warn("cap_rights_limit() failed");
348  			close(cachefd);
349  			return (EXIT_FAILURE);
350  		}
351  
352  #ifndef COVERAGE
353  		if (cap_enter() < 0 && errno != ENOSYS) {
354  			warn("cap_enter() failed");
355  			close(cachefd);
356  			return (EXIT_FAILURE);
357  		}
358  #endif
359  #endif
360  
361  	/* Build the list of out-of-date or obsolete packages */
362  
363  	recursive_analysis(cachefd, db, cachedir, cachedir, &dl, &sumlist, all,
364  	    &total);
365  	pkghash_destroy(sumlist);
366  
367  	if (dl.len == 0) {
368  		if (!quiet)
369  			printf("Nothing to do.\n");
370  		retcode = EXIT_SUCCESS;
371  		goto cleanup;
372  	}
373  
374  	humanize_number(size, sizeof(size), total, "B",
375  	    HN_AUTOSCALE, HN_IEC_PREFIXES);
376  
377  	if (!quiet)
378  		printf("The cleanup will free %s\n", size);
379  	if (!dry_run) {
380  			if (query_yesno(false,
381  			  "\nProceed with cleaning the cache? ")) {
382  				retcode = delete_dellist(cachefd, cachedir, &dl);
383  			}
384  	} else {
385  		retcode = EXIT_SUCCESS;
386  	}
387  
388  cleanup:
389  	pkgdb_release_lock(db, PKGDB_LOCK_READONLY);
390  	pkgdb_close(db);
391  	vec_free_and_free(&dl, free);
392  
393  	if (cachefd != -1)
394  		close(cachefd);
395  
396  	return (retcode);
397  }