Merge pull request #16933 from poettering/copy-hardlinks

copy: optionally recreate hardlinks when copying file trees
This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2020-09-10 19:04:52 +02:00
committed by GitHub
8 changed files with 290 additions and 28 deletions
+4 -1
View File
@@ -1654,7 +1654,10 @@ int btrfs_subvol_snapshot_fd_full(
} else if (r < 0)
return r;
r = copy_directory_fd_full(old_fd, new_path, COPY_MERGE|COPY_REFLINK|COPY_SAME_MOUNT|(FLAGS_SET(flags, BTRFS_SNAPSHOT_SIGINT) ? COPY_SIGINT : 0), progress_path, progress_bytes, userdata);
r = copy_directory_fd_full(
old_fd, new_path,
COPY_MERGE|COPY_REFLINK|COPY_SAME_MOUNT|COPY_HARDLINKS|(FLAGS_SET(flags, BTRFS_SNAPSHOT_SIGINT) ? COPY_SIGINT : 0),
progress_path, progress_bytes, userdata);
if (r < 0)
goto fallback_fail;
+236 -12
View File
@@ -22,8 +22,10 @@
#include "missing_syscall.h"
#include "mountpoint-util.h"
#include "nulstr-util.h"
#include "rm-rf.h"
#include "selinux-util.h"
#include "stat-util.h"
#include "stdio-util.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
@@ -394,6 +396,188 @@ static int fd_copy_symlink(
return 0;
}
/* Encapsulates the database we store potential hardlink targets in */
typedef struct HardlinkContext {
int dir_fd; /* An fd to the directory we use as lookup table. Never AT_FDCWD. Lazily created, when
* we add the first entry. */
/* These two fields are used to create the hardlink repository directory above — via
* mkdirat(parent_fd, subdir) — and are kept so that we can automatically remove the directory again
* when we are done. */
int parent_fd; /* Possibly AT_FDCWD */
char *subdir;
} HardlinkContext;
static int hardlink_context_setup(
HardlinkContext *c,
int dt,
const char *to,
CopyFlags copy_flags) {
_cleanup_close_ int dt_copy = -1;
int r;
assert(c);
assert(c->dir_fd < 0 && c->dir_fd != AT_FDCWD);
assert(c->parent_fd < 0);
assert(!c->subdir);
/* If hardlink recreation is requested we have to maintain a database of inodes that are potential
* hardlink sources. Given that generally disk sizes have to be assumed to be larger than what fits
* into physical RAM we cannot maintain that database in dynamic memory alone. Here we opt to
* maintain it on disk, to simplify things: inside the destination directory we'll maintain a
* temporary directory consisting of hardlinks of every inode we copied that might be subject of
* hardlinks. We can then use that as hardlink source later on. Yes, this means additional disk IO
* but thankfully Linux is optimized for this kind of thing. If this ever becomes a performance
* bottleneck we can certainly place an in-memory hash table in front of this, but for the beginning,
* let's keep things simple, and just use the disk as lookup table for inodes.
*
* Note that this should have zero performace impact as long as .n_link of all files copied remains
* <= 0, because in that case we will not actually allocate the hardlink inode lookup table directory
* on disk (we do so lazily, when the first candidate with .n_link > 1 is seen). This means, in the
* common case where hardlinks are not used at all or only for few files the fact that we store the
* table on disk shouldn't matter perfomance-wise. */
if (!FLAGS_SET(copy_flags, COPY_HARDLINKS))
return 0;
if (dt == AT_FDCWD)
dt_copy = AT_FDCWD;
else if (dt < 0)
return -EBADF;
else {
dt_copy = fcntl(dt, F_DUPFD_CLOEXEC, 3);
if (dt_copy < 0)
return -errno;
}
r = tempfn_random_child(to, "hardlink", &c->subdir);
if (r < 0)
return r;
c->parent_fd = TAKE_FD(dt_copy);
/* We don't actually create the directory we keep the table in here, that's done on-demand when the
* first entry is added, using hardlink_context_realize() below. */
return 1;
}
static int hardlink_context_realize(HardlinkContext *c) {
int r;
if (!c)
return 0;
if (c->dir_fd >= 0) /* Already realized */
return 1;
if (c->parent_fd < 0 && c->parent_fd != AT_FDCWD) /* Not configured */
return 0;
assert(c->subdir);
if (mkdirat(c->parent_fd, c->subdir, 0700) < 0)
return -errno;
c->dir_fd = openat(c->parent_fd, c->subdir, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
if (c->dir_fd < 0) {
r = -errno;
unlinkat(c->parent_fd, c->subdir, AT_REMOVEDIR);
return r;
}
return 1;
}
static void hardlink_context_destroy(HardlinkContext *c) {
int r;
assert(c);
/* Automatically remove the hardlink lookup table directory again after we are done. This is used via
* _cleanup_() so that we really delete this, even on failure. */
if (c->dir_fd >= 0) {
r = rm_rf_children(TAKE_FD(c->dir_fd), REMOVE_PHYSICAL, NULL); /* consumes dir_fd in all cases, even on failure */
if (r < 0)
log_debug_errno(r, "Failed to remove hardlink store (%s) contents, ignoring: %m", c->subdir);
assert(c->parent_fd >= 0 || c->parent_fd == AT_FDCWD);
assert(c->subdir);
if (unlinkat(c->parent_fd, c->subdir, AT_REMOVEDIR) < 0)
log_debug_errno(errno, "Failed to remove hardlink store (%s) directory, ignoring: %m", c->subdir);
}
assert_cc(AT_FDCWD < 0);
c->parent_fd = safe_close(c->parent_fd);
c->subdir = mfree(c->subdir);
}
static int try_hardlink(
HardlinkContext *c,
const struct stat *st,
int dt,
const char *to) {
char dev_ino[DECIMAL_STR_MAX(dev_t)*2 + DECIMAL_STR_MAX(uint64_t) + 4];
assert(st);
assert(dt >= 0 || dt == AT_FDCWD);
assert(to);
if (!c) /* No temporary hardlink directory, don't bother */
return 0;
if (st->st_nlink <= 1) /* Source not hardlinked, don't bother */
return 0;
if (c->dir_fd < 0) /* not yet realized, hence empty */
return 0;
xsprintf(dev_ino, "%u:%u:%" PRIu64, major(st->st_dev), minor(st->st_dev), (uint64_t) st->st_ino);
if (linkat(c->dir_fd, dev_ino, dt, to, 0) < 0) {
if (errno != ENOENT) /* doesn't exist in store yet */
log_debug_errno(errno, "Failed to hardlink %s to %s, ignoring: %m", dev_ino, to);
return 0;
}
return 1;
}
static int memorize_hardlink(
HardlinkContext *c,
const struct stat *st,
int dt,
const char *to) {
char dev_ino[DECIMAL_STR_MAX(dev_t)*2 + DECIMAL_STR_MAX(uint64_t) + 4];
int r;
assert(st);
assert(dt >= 0 || dt == AT_FDCWD);
assert(to);
if (!c) /* No temporary hardlink directory, don't bother */
return 0;
if (st->st_nlink <= 1) /* Source not hardlinked, don't bother */
return 0;
r = hardlink_context_realize(c); /* Create the hardlink store lazily */
if (r < 0)
return r;
xsprintf(dev_ino, "%u:%u:%" PRIu64, major(st->st_dev), minor(st->st_dev), (uint64_t) st->st_ino);
if (linkat(dt, to, c->dir_fd, dev_ino, 0) < 0) {
log_debug_errno(errno, "Failed to hardlink %s to %s, ignoring: %m", to, dev_ino);
return 0;
}
return 1;
}
static int fd_copy_regular(
int df,
const char *from,
@@ -403,6 +587,7 @@ static int fd_copy_regular(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
HardlinkContext *hardlink_context,
copy_progress_bytes_t progress,
void *userdata) {
@@ -414,6 +599,12 @@ static int fd_copy_regular(
assert(st);
assert(to);
r = try_hardlink(hardlink_context, st, dt, to);
if (r < 0)
return r;
if (r > 0) /* worked! */
return 0;
fdf = openat(df, from, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW);
if (fdf < 0)
return -errno;
@@ -456,6 +647,7 @@ static int fd_copy_regular(
(void) unlinkat(dt, to, 0);
}
(void) memorize_hardlink(hardlink_context, st, dt, to);
return r;
}
@@ -467,13 +659,20 @@ static int fd_copy_fifo(
const char *to,
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags) {
CopyFlags copy_flags,
HardlinkContext *hardlink_context) {
int r;
assert(from);
assert(st);
assert(to);
r = try_hardlink(hardlink_context, st, dt, to);
if (r < 0)
return r;
if (r > 0) /* worked! */
return 0;
if (copy_flags & COPY_MAC_CREATE) {
r = mac_selinux_create_file_prepare_at(dt, to, S_IFIFO);
if (r < 0)
@@ -494,6 +693,7 @@ static int fd_copy_fifo(
if (fchmodat(dt, to, st->st_mode & 07777, 0) < 0)
r = -errno;
(void) memorize_hardlink(hardlink_context, st, dt, to);
return r;
}
@@ -505,13 +705,20 @@ static int fd_copy_node(
const char *to,
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags) {
CopyFlags copy_flags,
HardlinkContext *hardlink_context) {
int r;
assert(from);
assert(st);
assert(to);
r = try_hardlink(hardlink_context, st, dt, to);
if (r < 0)
return r;
if (r > 0) /* worked! */
return 0;
if (copy_flags & COPY_MAC_CREATE) {
r = mac_selinux_create_file_prepare_at(dt, to, st->st_mode & S_IFMT);
if (r < 0)
@@ -532,6 +739,7 @@ static int fd_copy_node(
if (fchmodat(dt, to, st->st_mode & 07777, 0) < 0)
r = -errno;
(void) memorize_hardlink(hardlink_context, st, dt, to);
return r;
}
@@ -546,11 +754,17 @@ static int fd_copy_directory(
uid_t override_uid,
gid_t override_gid,
CopyFlags copy_flags,
HardlinkContext *hardlink_context,
const char *display_path,
copy_progress_path_t progress_path,
copy_progress_bytes_t progress_bytes,
void *userdata) {
_cleanup_(hardlink_context_destroy) HardlinkContext our_hardlink_context = {
.dir_fd = -1,
.parent_fd = -1,
};
_cleanup_close_ int fdf = -1, fdt = -1;
_cleanup_closedir_ DIR *d = NULL;
struct dirent *de;
@@ -570,6 +784,16 @@ static int fd_copy_directory(
if (fdf < 0)
return -errno;
if (!hardlink_context) {
/* If recreating hardlinks is requested let's set up a context for that now. */
r = hardlink_context_setup(&our_hardlink_context, dt, to, copy_flags);
if (r < 0)
return r;
if (r > 0) /* It's enabled and allocated, let's now use the same context for all recursive
* invocations from here down */
hardlink_context = &our_hardlink_context;
}
d = take_fdopendir(&fdf);
if (!d)
return -errno;
@@ -668,15 +892,15 @@ static int fd_copy_directory(
continue;
}
q = fd_copy_directory(dirfd(d), de->d_name, &buf, fdt, de->d_name, original_device, depth_left-1, override_uid, override_gid, copy_flags, child_display_path, progress_path, progress_bytes, userdata);
q = fd_copy_directory(dirfd(d), de->d_name, &buf, fdt, de->d_name, original_device, depth_left-1, override_uid, override_gid, copy_flags, hardlink_context, child_display_path, progress_path, progress_bytes, userdata);
} else if (S_ISREG(buf.st_mode))
q = fd_copy_regular(dirfd(d), de->d_name, &buf, fdt, de->d_name, override_uid, override_gid, copy_flags, progress_bytes, userdata);
q = fd_copy_regular(dirfd(d), de->d_name, &buf, fdt, de->d_name, override_uid, override_gid, copy_flags, hardlink_context, progress_bytes, userdata);
else if (S_ISLNK(buf.st_mode))
q = fd_copy_symlink(dirfd(d), de->d_name, &buf, fdt, de->d_name, override_uid, override_gid, copy_flags);
else if (S_ISFIFO(buf.st_mode))
q = fd_copy_fifo(dirfd(d), de->d_name, &buf, fdt, de->d_name, override_uid, override_gid, copy_flags);
q = fd_copy_fifo(dirfd(d), de->d_name, &buf, fdt, de->d_name, override_uid, override_gid, copy_flags, hardlink_context);
else if (S_ISBLK(buf.st_mode) || S_ISCHR(buf.st_mode) || S_ISSOCK(buf.st_mode))
q = fd_copy_node(dirfd(d), de->d_name, &buf, fdt, de->d_name, override_uid, override_gid, copy_flags);
q = fd_copy_node(dirfd(d), de->d_name, &buf, fdt, de->d_name, override_uid, override_gid, copy_flags, hardlink_context);
else
q = -EOPNOTSUPP;
@@ -730,15 +954,15 @@ int copy_tree_at_full(
return -errno;
if (S_ISREG(st.st_mode))
return fd_copy_regular(fdf, from, &st, fdt, to, override_uid, override_gid, copy_flags, progress_bytes, userdata);
return fd_copy_regular(fdf, from, &st, fdt, to, override_uid, override_gid, copy_flags, NULL, progress_bytes, userdata);
else if (S_ISDIR(st.st_mode))
return fd_copy_directory(fdf, from, &st, fdt, to, st.st_dev, COPY_DEPTH_MAX, override_uid, override_gid, copy_flags, NULL, progress_path, progress_bytes, userdata);
return fd_copy_directory(fdf, from, &st, fdt, to, st.st_dev, COPY_DEPTH_MAX, override_uid, override_gid, copy_flags, NULL, NULL, progress_path, progress_bytes, userdata);
else if (S_ISLNK(st.st_mode))
return fd_copy_symlink(fdf, from, &st, fdt, to, override_uid, override_gid, copy_flags);
else if (S_ISFIFO(st.st_mode))
return fd_copy_fifo(fdf, from, &st, fdt, to, override_uid, override_gid, copy_flags);
return fd_copy_fifo(fdf, from, &st, fdt, to, override_uid, override_gid, copy_flags, NULL);
else if (S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode) || S_ISSOCK(st.st_mode))
return fd_copy_node(fdf, from, &st, fdt, to, override_uid, override_gid, copy_flags);
return fd_copy_node(fdf, from, &st, fdt, to, override_uid, override_gid, copy_flags, NULL);
else
return -EOPNOTSUPP;
}
@@ -762,7 +986,7 @@ int copy_directory_fd_full(
if (!S_ISDIR(st.st_mode))
return -ENOTDIR;
return fd_copy_directory(dirfd, NULL, &st, AT_FDCWD, to, st.st_dev, COPY_DEPTH_MAX, UID_INVALID, GID_INVALID, copy_flags, NULL, progress_path, progress_bytes, userdata);
return fd_copy_directory(dirfd, NULL, &st, AT_FDCWD, to, st.st_dev, COPY_DEPTH_MAX, UID_INVALID, GID_INVALID, copy_flags, NULL, NULL, progress_path, progress_bytes, userdata);
}
int copy_directory_full(
@@ -784,7 +1008,7 @@ int copy_directory_full(
if (!S_ISDIR(st.st_mode))
return -ENOTDIR;
return fd_copy_directory(AT_FDCWD, from, &st, AT_FDCWD, to, st.st_dev, COPY_DEPTH_MAX, UID_INVALID, GID_INVALID, copy_flags, NULL, progress_path, progress_bytes, userdata);
return fd_copy_directory(AT_FDCWD, from, &st, AT_FDCWD, to, st.st_dev, COPY_DEPTH_MAX, UID_INVALID, GID_INVALID, copy_flags, NULL, NULL, progress_path, progress_bytes, userdata);
}
int copy_file_fd_full(
+1
View File
@@ -17,6 +17,7 @@ typedef enum CopyFlags {
COPY_CRTIME = 1 << 5, /* Generate a user.crtime_usec xattr off the source crtime if there is one, on copying */
COPY_SIGINT = 1 << 6, /* Check for SIGINT regularly and return EINTR if seen (caller needs to block SIGINT) */
COPY_MAC_CREATE = 1 << 7, /* Create files with the correct MAC label (currently SELinux only) */
COPY_HARDLINKS = 1 << 8, /* Try to reproduce hard links */
} CopyFlags;
typedef int (*copy_progress_bytes_t)(uint64_t n_bytes, void *userdata);