journal-vacuum.c revision 2b43f939a4b3ad5aeb2650868b0234ff42ec0045
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen/***
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen This file is part of systemd.
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen Copyright 2011 Lennart Poettering
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen systemd is free software; you can redistribute it and/or modify it
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen under the terms of the GNU Lesser General Public License as published by
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen the Free Software Foundation; either version 2.1 of the License, or
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen (at your option) any later version.
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen systemd is distributed in the hope that it will be useful, but
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen WITHOUT ANY WARRANTY; without even the implied warranty of
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen Lesser General Public License for more details.
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen You should have received a copy of the GNU Lesser General Public License
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen along with systemd; If not, see <http://www.gnu.org/licenses/>.
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen***/
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include <sys/types.h>
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include <fcntl.h>
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include <sys/stat.h>
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include <sys/statvfs.h>
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include <unistd.h>
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include "journal-def.h"
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include "journal-file.h"
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include "journal-vacuum.h"
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include "sd-id128.h"
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen#include "util.h"
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersenstruct vacuum_info {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen off_t usage;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen char *filename;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen uint64_t realtime;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen sd_id128_t seqnum_id;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen uint64_t seqnum;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen bool have_seqnum;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen};
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersenstatic int vacuum_compare(const void *_a, const void *_b) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen const struct vacuum_info *a, *b;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen a = _a;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen b = _b;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (a->have_seqnum && b->have_seqnum &&
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen sd_id128_equal(a->seqnum_id, b->seqnum_id)) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (a->seqnum < b->seqnum)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return -1;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen else if (a->seqnum > b->seqnum)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return 1;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen else
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return 0;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (a->realtime < b->realtime)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return -1;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen else if (a->realtime > b->realtime)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return 1;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen else if (a->have_seqnum && b->have_seqnum)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return memcmp(&a->seqnum_id, &b->seqnum_id, 16);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen else
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return strcmp(a->filename, b->filename);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen}
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersenint journal_directory_vacuum(const char *directory, uint64_t max_use, uint64_t min_free) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen DIR *d;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen int r = 0;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen struct vacuum_info *list = NULL;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen unsigned n_list = 0, n_allocated = 0, i;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen uint64_t sum = 0;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen assert(directory);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (max_use <= 0)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return 0;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen d = opendir(directory);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (!d)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return -errno;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen for (;;) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen int k;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen struct dirent buf, *de;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen size_t q;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen struct stat st;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen char *p;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen unsigned long long seqnum = 0, realtime;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen sd_id128_t seqnum_id;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen bool have_seqnum;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen k = readdir_r(d, &buf, &de);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (k != 0) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen r = -k;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen goto finish;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (!de)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen break;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (fstatat(dirfd(d), de->d_name, &st, AT_SYMLINK_NOFOLLOW) < 0)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (!S_ISREG(st.st_mode))
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen q = strlen(de->d_name);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (endswith(de->d_name, ".journal")) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen /* Vacuum archived files */
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (q < 1 + 32 + 1 + 16 + 1 + 16 + 8)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (de->d_name[q-8-16-1] != '-' ||
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen de->d_name[q-8-16-1-16-1] != '-' ||
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen de->d_name[q-8-16-1-16-1-32-1] != '@')
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen p = strdup(de->d_name);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (!p) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen r = -ENOMEM;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen goto finish;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen de->d_name[q-8-16-1-16-1] = 0;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (sd_id128_from_string(de->d_name + q-8-16-1-16-1-32, &seqnum_id) < 0) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen free(p);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (sscanf(de->d_name + q-8-16-1-16, "%16llx-%16llx.journal", &seqnum, &realtime) != 2) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen free(p);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen have_seqnum = true;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen } else if (endswith(de->d_name, ".journal~")) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen unsigned long long tmp;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen /* Vacuum corrupted files */
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (q < 1 + 16 + 1 + 16 + 8 + 1)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (de->d_name[q-1-8-16-1] != '-' ||
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen de->d_name[q-1-8-16-1-16-1] != '@')
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen p = strdup(de->d_name);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (!p) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen r = -ENOMEM;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen goto finish;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (sscanf(de->d_name + q-1-8-16-1-16, "%16llx-%16llx.journal~", &realtime, &tmp) != 2) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen free(p);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen have_seqnum = false;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen } else
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen continue;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (n_list >= n_allocated) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen struct vacuum_info *j;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen n_allocated = MAX(n_allocated * 2U, 8U);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen j = realloc(list, n_allocated * sizeof(struct vacuum_info));
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (!j) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen free(p);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen r = -ENOMEM;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen goto finish;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen list = j;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen list[n_list].filename = p;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen list[n_list].usage = 512UL * (uint64_t) st.st_blocks;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen list[n_list].seqnum = seqnum;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen list[n_list].realtime = realtime;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen list[n_list].seqnum_id = seqnum_id;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen list[n_list].have_seqnum = have_seqnum;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen sum += list[n_list].usage;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen n_list ++;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (n_list > 0)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen qsort(list, n_list, sizeof(struct vacuum_info), vacuum_compare);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen for(i = 0; i < n_list; i++) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen struct statvfs ss;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (fstatvfs(dirfd(d), &ss) < 0) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen r = -errno;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen goto finish;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (sum <= max_use &&
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen (uint64_t) ss.f_bavail * (uint64_t) ss.f_bsize >= min_free)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen break;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (unlinkat(dirfd(d), list[i].filename, 0) >= 0) {
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen log_debug("Deleted archived journal %s/%s.", directory, list[i].filename);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen sum -= list[i].usage;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen } else if (errno != ENOENT)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen log_warning("Failed to delete %s/%s: %m", directory, list[i].filename);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen }
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersenfinish:
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen for (i = 0; i < n_list; i++)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen free(list[i].filename);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen free(list);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen if (d)
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen closedir(d);
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen return r;
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen}
57fa1d094cd2c5ac68970526ad0a0754c548e75dTom Gundersen