Files
rsync/hlink.c
T
fbc7f58dd1 hlink: recover from failed hard-link creation (#1144)
* hlink: fix hard-link failure abort (#741) and the latent drain hang it exposes

When a hard-link group member fails its direct link() (EMLINK or
EXDEV) it is written as a regular file and finishes first, claiming
the group's prior_hlinks node; a later member's finish then hits the
"Hlink node data ... already has path" check and aborts the whole
transfer (code 13).  Tolerate the duplicate finish: keep the first
leader's path, log an FINFO warning, and continue.

Removing the abort exposes a second, latent bug: finish_hard_link()
decrements flist->in_progress for every file in the completion chain,
but members that failed their direct link() in hard_link_check() were
never counted, so the counter drifts negative and the final drain
hangs forever.  Add FLAG_HLINK_WAIT, set at the three increment
sites, and gate the two affected decrements on it.

Tested with a 65000-link pinned-inode EMLINK fixture (full repro and
evidence in the PR description), an -H --link-dest regression matrix,
and the upstream hardlink/link-dest test suite.
hlink: fix hard-link failure abort (#741) and the latent drain hang it exposes

When a hard-link group member fails its direct link() (EMLINK or
EXDEV) it is written as a regular file and finishes first, claiming
the group's prior_hlinks node; a later member's finish then hits the
"Hlink node data ... already has path" check and aborts the whole
transfer (code 13).  Tolerate the duplicate finish: keep the first
leader's path, log an FINFO warning, and continue.

Removing the abort exposes a second, latent bug: finish_hard_link()
decrements flist->in_progress for every file in the completion chain,
but members that failed their direct link() in hard_link_check() were
never counted, so the counter drifts negative and the final drain
hangs forever.  Add FLAG_HLINK_WAIT, set at the three increment
sites, and gate the two affected decrements on it.

Tested with a 65000-link pinned-inode EMLINK fixture (full repro and
evidence in the PR description), an -H --link-dest regression matrix,
and the upstream hardlink/link-dest test suite.

* hlink: re-root a hard-link group whose root is in a released file list

Address the review comment on commit 350425a8 (RsyncProject/rsync#1144):
"This will still abort when the hard-link group spans incremental
file-list chunks.  Re-root the group without retaining released
file-list indices."

When a group spans chunks, a member's F_HL_PREV can point into a
file-list that is released before the member's finish_hard_link()
walks the completion chain, and the fatal flist_for_ndx() call
aborts the transfer.  Two changes in finish_hard_link():

1. The completion-chain lookup now uses the graceful (NULL) variant
   of flist_for_ndx() and stops the walk when an index is no longer
   in a live file-list.  The chain only moves to earlier ndx values,
   so once one is released, everything further back is already
   finished and there is nothing left to re-link.  No live file
   retains a released index: the finishing file's F_HL_PREV is reset
   on entry, and each walked member's F_HL_PREV is pointed at the
   finishing file.

2. A duplicate finish (the group already recorded as finished at
   some path) now re-roots the group at the newly finished file
   instead of keeping the recorded path.  A duplicate finish only
   happens when the recorded root could not be hard-linked (e.g.
   EMLINK, EXDEV) and that member was written as a regular file, so
   that member's inode is the only one the group can still link to;
   later members then link to this live file instead of the stale
   root.

The abort is reproducible locally with a small chunk-spanning fixture
whose first file-list is under MIN_FILECNT_LOOKAHEAD/2, both on the
series base 8718cd28 and with only commit 350425a8 applied: the
hard-link lookahead at the top of generate_files() then matches the
extra file-list before the group leader is itemized, so the members'
F_HL_PREV pointers into the leader's file-list survive the match,
and the fatal lookup aborts deterministically: "File-list index 3
not in 11 - 13 (finish_hard_link)" with "protocol incompatibility"
(code 2).  With this commit the same run completes with rc 23 and
the group re-rooted on a fresh inode; a simulated WAN delay for the
last member gives the identical abort/fix behavior (fixture and full
evidence in the description of RsyncProject/rsync#1144).  Also
verified: a 1200-file chunk-spanning EMLINK fixture with the
destination inode pinned at the ext4 65000-link cap completes in
0.2 s with rc 23 and the group re-rooted on a fresh inode; the
single-flist EMLINK fixture takes 0.15 s, rc 23, all 101 members on
one fresh inode; the hard-link regression tree is all rc 0 with no
spurious output; the upstream hardlink/link-dest/keep-dirlinks suite
(9 tests) passes.

Co-authored-by: LIM Coding Assistant <298007100+statefullm@users.noreply.github.com>

* hlink: cut stale F_HL_PREV indexes into released file lists

Address the review comment on ad01ac16 (RsyncProject/rsync#1144):
"Breaking here only hides the stale index."

The break added by ad01ac16 stops the finish_hard_link() walk from
dereferencing a released file-list, but it only avoided the stale
index: a live file_struct could still hold its F_HL_PREV into a
released list (for example, a FLAG_HLINK_WAIT member keeps its own
file-list alive while an earlier member's file-list is released),
and the index sat in place until that member's own completion
re-pointed or reset it.  That is harmless in practice - every
F_HL_PREV reader uses the NULL-tolerant flist_for_ndx() - but the
index is now cut instead of retained, at the places where a live
file_struct reads or re-acquires one:

1. check_prior() resets F_HL_PREV to -1 when its walk ends because
   the target index is no longer in a live file-list (previously the
   stale value was retained when the group had already recorded a
   finished path).

2. The FLAG_HLINK_WAIT re-point in hard_link_check() copies the
   previous member's F_HL_PREV only while that index is still in a
   live list, and cuts it to -1 otherwise.

Cutting is safe because a file-list is released only after every
file in it has completed: check_for_finished_files() gates
flist_free() on in_progress and to_redo being zero, and in_progress
counts every sent file and every FLAG_HLINK_WAIT file.  So the
members of a released list are all finished and the chain has
nothing left to do there.  The finish_hard_link() walk itself
re-points each walked member at the finishing file before testing
the next hop, and the finishing file's F_HL_PREV is reset on entry,
so after the walk no live file_struct points into a released list;
its break remains as the last resort where an earlier member's index
is already gone.

Verified: the small chunk-spanning fixture (first file-list under
MIN_FILECNT_LOOKAHEAD/2) completes with rc 23 and the group re-rooted
on a fresh inode, with a temporary probe confirming that at the
walk's break no live file_struct retains an index into a released
list (every live hlinked file's F_HL_PREV is either -1 or in a live
list); the 1200-file chunk-spanning EMLINK fixture and the
single-flist EMLINK fixture (101 members on one fresh inode) behave
as before with rc 23 and the group re-rooted; the hard-link
regression tree is all rc 0 with no spurious output; the upstream
hardlink/link-dest/keep-dirlinks suite (9 tests) passes.

Co-authored-by: LIM Coding Assistant <298007100+statefullm@users.noreply.github.com>

* hlink: transfer files after hard-link failure

---------

Co-authored-by: LIM Coding Assistant <298007100+statefullm@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-11-03 18:11:59 +10:00

622 lines
18 KiB
C

/*
* Routines to support hard-linking.
*
* Copyright (C) 1996 Andrew Tridgell
* Copyright (C) 1996 Paul Mackerras
* Copyright (C) 2002 Martin Pool <mbp@samba.org>
* Copyright (C) 2004-2022 Wayne Davison
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, visit the http://fsf.org website.
*/
#include "rsync.h"
#include "inums.h"
#include "ifuncs.h"
extern int dry_run;
extern int list_only;
extern int am_sender;
extern int inc_recurse;
extern int do_xfers;
extern int alt_dest_type;
extern int preserve_acls;
extern int preserve_xattrs;
extern int protocol_version;
extern int remove_source_files;
extern int stdout_format_has_i;
extern int maybe_ATTRS_REPORT;
extern int unsort_ndx;
extern char *basis_dir[MAX_BASIS_DIRS+1];
extern struct file_list *cur_flist;
#ifdef SUPPORT_HARD_LINKS
/* Starting with protocol 30, we use a simple hashtable on the sending side
* for hashing the st_dev and st_ino info. The receiving side gets told
* (via flags and a "group index") which items are hard-linked together, so
* we can avoid the pool of dev+inode data. For incremental recursion mode,
* the receiver will use a ndx hash to remember old pathnames. */
static void *data_when_new = "";
static struct hashtable *dev_tbl;
static struct hashtable *prior_hlinks;
static struct file_list *hlink_flist;
void init_hard_links(void)
{
if (am_sender || protocol_version < 30)
dev_tbl = hashtable_create(16, HT_KEY64);
else if (inc_recurse)
prior_hlinks = hashtable_create(1024, HT_KEY32);
}
struct ht_int64_node *idev_find(int64 dev, int64 ino)
{
static struct ht_int64_node *dev_node = NULL;
/* Note that some OSes have a dev == 0, so increment to avoid storing a 0. */
if (!dev_node || dev_node->key != dev+1) {
/* We keep a separate hash table of inodes for every device. */
dev_node = hashtable_find(dev_tbl, dev+1, data_when_new);
if (dev_node->data == data_when_new) {
dev_node->data = hashtable_create(512, HT_KEY64);
if (DEBUG_GTE(HLINK, 3)) {
rprintf(FINFO, "[%s] created hashtable for dev %s\n",
who_am_i(), big_num(dev));
}
}
}
return hashtable_find(dev_node->data, ino, (void*)-1L);
}
void idev_destroy(void)
{
int i;
for (i = 0; i < dev_tbl->size; i++) {
struct ht_int32_node *node = HT_NODE(dev_tbl, dev_tbl->nodes, i);
if (node->data)
hashtable_destroy(node->data);
}
hashtable_destroy(dev_tbl);
}
static int hlink_compare_gnum(int *int1, int *int2)
{
struct file_struct *f1 = hlink_flist->sorted[*int1];
struct file_struct *f2 = hlink_flist->sorted[*int2];
int32 gnum1 = F_HL_GNUM(f1);
int32 gnum2 = F_HL_GNUM(f2);
if (gnum1 != gnum2)
return gnum1 > gnum2 ? 1 : -1;
return *int1 > *int2 ? 1 : -1;
}
static void match_gnums(int32 *ndx_list, int ndx_count)
{
int32 from, prev;
struct file_struct *file, *file_next;
struct ht_int32_node *node = NULL;
int32 gnum, gnum_next;
qsort(ndx_list, ndx_count, sizeof ndx_list[0], (int (*)(const void*, const void*))hlink_compare_gnum);
for (from = 0; from < ndx_count; from++) {
file = hlink_flist->sorted[ndx_list[from]];
gnum = F_HL_GNUM(file);
if (inc_recurse) {
node = hashtable_find(prior_hlinks, gnum, data_when_new);
if (node->data == data_when_new) {
if (gnum < hlink_flist->ndx_start) {
/* A non-first hard-link entry whose
* gnum points before this flist's
* ndx_start should already have been
* recorded in prior_hlinks by an
* earlier flist. A peer that sends
* such a back-reference on the first
* flist (or to a gnum that was never
* declared XMIT_HLINK_FIRST) is
* misbehaving. */
rprintf(FERROR,
"hard-link gnum %d precedes flist start %d\n",
(int)gnum, (int)hlink_flist->ndx_start);
exit_cleanup(RERR_PROTOCOL);
}
node->data = new_array0(char, 5);
file->flags |= FLAG_HLINK_FIRST;
prev = -1;
} else if (CVAL(node->data, 0) == 0) {
struct file_list *flist;
prev = IVAL(node->data, 1);
flist = flist_for_ndx(prev, NULL);
if (flist)
flist->files[prev - flist->ndx_start]->flags &= ~FLAG_HLINK_LAST;
else {
/* We skipped all prior files in this
* group, so mark this as a "first". */
file->flags |= FLAG_HLINK_FIRST;
prev = -1;
}
} else
prev = -1;
} else {
file->flags |= FLAG_HLINK_FIRST;
prev = -1;
}
for ( ; from < ndx_count-1; file = file_next, gnum = gnum_next, from++) { /*SHARED ITERATOR*/
file_next = hlink_flist->sorted[ndx_list[from+1]];
gnum_next = F_HL_GNUM(file_next);
if (gnum != gnum_next)
break;
F_HL_PREV(file) = prev;
/* The linked list uses over-the-wire ndx values. */
if (unsort_ndx)
prev = F_NDX(file);
else
prev = ndx_list[from] + hlink_flist->ndx_start;
}
if (prev < 0 && !inc_recurse) {
/* Disable hard-link bit and set DONE so that
* HLINK_BUMP()-dependent values are unaffected. */
file->flags &= ~(FLAG_HLINKED | FLAG_HLINK_FIRST);
file->flags |= FLAG_HLINK_DONE;
continue;
}
file->flags |= FLAG_HLINK_LAST;
F_HL_PREV(file) = prev;
if (inc_recurse && CVAL(node->data, 0) == 0) {
if (unsort_ndx)
prev = F_NDX(file);
else
prev = ndx_list[from] + hlink_flist->ndx_start;
SIVAL(node->data, 1, prev);
}
}
}
/* Analyze the hard-links in the file-list by creating a list of all the
* items that have hlink data, sorting them, and matching up identical
* values into clusters. These will be a single linked list from last
* to first when we're done. */
void match_hard_links(struct file_list *flist)
{
if (!list_only && flist->used) {
int i, ndx_count = 0;
int32 *ndx_list;
ndx_list = new_array(int32, flist->used);
for (i = 0; i < flist->used; i++) {
if (F_IS_HLINKED(flist->sorted[i]))
ndx_list[ndx_count++] = i;
}
hlink_flist = flist;
if (ndx_count)
match_gnums(ndx_list, ndx_count);
free(ndx_list);
}
if (protocol_version < 30)
idev_destroy();
}
static int maybe_hard_link(struct file_struct *file, int ndx,
char *fname, int statret, stat_x *sxp,
const char *oldname, STRUCT_STAT *old_stp,
const char *realname, int itemizing, enum logcode code)
{
if (statret == 0) {
if (sxp->st.st_dev == old_stp->st_dev
&& sxp->st.st_ino == old_stp->st_ino) {
if (itemizing) {
itemize(fname, file, ndx, statret, sxp,
ITEM_LOCAL_CHANGE | ITEM_XNAME_FOLLOWS,
0, "");
}
if (INFO_GTE(NAME, 2) && maybe_ATTRS_REPORT)
rprintf(FCLIENT, "%s is uptodate\n", fname);
file->flags |= FLAG_HLINK_DONE;
return 0;
}
}
if (atomic_create(file, fname, NULL, oldname, MAKEDEV(0, 0), sxp, statret == 0 ? DEL_FOR_FILE : 0)) {
if (itemizing) {
itemize(fname, file, ndx, statret, sxp,
ITEM_LOCAL_CHANGE | ITEM_XNAME_FOLLOWS, 0,
realname);
}
if (code != FNONE && INFO_GTE(NAME, 1))
rprintf(code, "%s => %s\n", fname, realname);
return 0;
}
return -1;
}
/* Figure out if a prior entry is still there or if we just have a
* cached name for it. */
static char *check_prior(struct file_struct *file, int gnum,
int *prev_ndx_p, struct file_list **flist_p)
{
struct file_struct *fp;
struct ht_int32_node *node;
int prev_ndx = F_HL_PREV(file);
while (1) {
struct file_list *flist;
if (prev_ndx < 0
|| (flist = flist_for_ndx(prev_ndx, NULL)) == NULL)
break;
fp = flist->files[prev_ndx - flist->ndx_start];
if (!(fp->flags & FLAG_SKIP_HLINK)) {
*prev_ndx_p = prev_ndx;
*flist_p = flist;
return NULL;
}
F_HL_PREV(file) = prev_ndx = F_HL_PREV(fp);
}
if (prev_ndx >= 0) {
/* The prior member is no longer in a live file-list, which
* only happens once all of its files have completed: cut the
* stale index instead of retaining it. */
F_HL_PREV(file) = -1;
}
if (inc_recurse
&& (node = hashtable_find(prior_hlinks, gnum, NULL)) != NULL) {
assert(node->data != NULL);
if (CVAL(node->data, 0) != 0) {
*prev_ndx_p = -1;
*flist_p = NULL;
return node->data;
}
/* The prior file must have been skipped. */
F_HL_PREV(file) = -1;
}
*prev_ndx_p = -1;
*flist_p = NULL;
return NULL;
}
/* Only called if FLAG_HLINKED is set and FLAG_HLINK_FIRST is not. Returns:
* 0 = process the file, 1 = skip the file, -1 = error occurred. */
int hard_link_check(struct file_struct *file, int ndx, char *fname,
int statret, stat_x *sxp, int itemizing,
enum logcode code)
{
STRUCT_STAT prev_st;
char namebuf[MAXPATHLEN], altbuf[MAXPATHLEN];
char *realname, *prev_name;
struct file_list *flist;
int gnum = inc_recurse ? F_HL_GNUM(file) : -1;
int prev_ndx;
prev_name = realname = check_prior(file, gnum, &prev_ndx, &flist);
if (!prev_name) {
struct file_struct *prev_file;
if (!flist) {
/* The previous file was skipped, so this one is
* treated as if it were the first in its group. */
if (DEBUG_GTE(HLINK, 2)) {
rprintf(FINFO, "hlink for %d (%s,%d): virtual first\n",
ndx, f_name(file, NULL), gnum);
}
return 0;
}
prev_file = flist->files[prev_ndx - flist->ndx_start];
/* Is the previous link not complete yet? */
if (!(prev_file->flags & FLAG_HLINK_DONE)) {
/* Is the previous link being transferred? */
if (prev_file->flags & FLAG_FILE_SENT) {
/* Add ourselves to the list of files that will
* be updated when the transfer completes, and
* mark ourself as waiting for the transfer. */
if (F_HL_PREV(prev_file) >= 0
&& flist_for_ndx(F_HL_PREV(prev_file), NULL))
F_HL_PREV(file) = F_HL_PREV(prev_file);
else
/* prev_file's prior member (if any) is
* already finished: cut the chain here. */
F_HL_PREV(file) = -1;
F_HL_PREV(prev_file) = ndx;
file->flags |= FLAG_FILE_SENT;
file->flags |= FLAG_HLINK_WAIT;
cur_flist->in_progress++;
if (DEBUG_GTE(HLINK, 2)) {
rprintf(FINFO, "hlink for %d (%s,%d): waiting for %d\n",
ndx, f_name(file, NULL), gnum, F_HL_PREV(file));
}
return 1;
}
if (DEBUG_GTE(HLINK, 2)) {
rprintf(FINFO, "hlink for %d (%s,%d): looking for a leader\n",
ndx, f_name(file, NULL), gnum);
}
return 0;
}
/* There is a finished file to link with! */
if (!(prev_file->flags & FLAG_HLINK_FIRST)) {
/* The previous previous is FIRST when prev is not. */
prev_name = realname = check_prior(prev_file, gnum, &prev_ndx, &flist);
/* Update our previous pointer to point to the FIRST. */
F_HL_PREV(file) = prev_ndx;
}
if (!prev_name) {
int alt_dest;
assert(flist != NULL);
prev_file = flist->files[prev_ndx - flist->ndx_start];
/* F_HL_PREV() is alt_dest value when DONE && FIRST. */
alt_dest = F_HL_PREV(prev_file);
if (DEBUG_GTE(HLINK, 2)) {
rprintf(FINFO, "hlink for %d (%s,%d): found flist match (alt %d)\n",
ndx, f_name(file, NULL), gnum, alt_dest);
}
if (alt_dest >= 0 && dry_run) {
pathjoin(namebuf, MAXPATHLEN, basis_dir[alt_dest],
f_name(prev_file, NULL));
prev_name = namebuf;
realname = f_name(prev_file, altbuf);
} else {
prev_name = f_name(prev_file, namebuf);
realname = prev_name;
}
}
}
if (DEBUG_GTE(HLINK, 2)) {
rprintf(FINFO, "hlink for %d (%s,%d): leader is %d (%s)\n",
ndx, f_name(file, NULL), gnum, prev_ndx, prev_name);
}
if (link_stat(prev_name, &prev_st, 0) < 0) {
if (!dry_run || errno != ENOENT) {
rsyserr(FERROR_XFER, errno, "stat %s failed", full_fname(prev_name));
return -1;
}
/* A new hard-link will get a new dev & inode, so approximate
* those values in dry-run mode by zeroing them. */
memset(&prev_st, 0, sizeof prev_st);
}
if (statret < 0 && basis_dir[0] != NULL) {
/* If we match an alt-dest item, we don't output this as a change. */
char cmpbuf[MAXPATHLEN];
stat_x alt_sx;
int j = 0;
init_stat_x(&alt_sx);
do {
pathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);
if (link_stat(cmpbuf, &alt_sx.st, 0) < 0)
continue;
if (alt_dest_type == LINK_DEST) {
if (prev_st.st_dev != alt_sx.st.st_dev
|| prev_st.st_ino != alt_sx.st.st_ino)
continue;
statret = 1;
if (stdout_format_has_i == 0
|| (!INFO_GTE(NAME, 2) && stdout_format_has_i < 2)) {
itemizing = 0;
code = FNONE;
if (INFO_GTE(NAME, 2) && maybe_ATTRS_REPORT)
rprintf(FCLIENT, "%s is uptodate\n", fname);
}
break;
}
/* Content-based basis match only applies to regular
* files: for a hard-linked symlink/device/special the
* exact-inode check above is the only meaningful test,
* and quick_check_ok(FT_REG, ...) would read F_SUM()
* on a file_struct that has no SUM_EXTRA_CNT space
* (recv_file_entry only allocates it for S_ISREG). */
if (!S_ISREG(file->mode)
|| !quick_check_ok(FT_REG, cmpbuf, file, &alt_sx.st))
continue;
statret = 1;
if (unchanged_attrs(cmpbuf, file, &alt_sx))
break;
} while (basis_dir[++j] != NULL);
if (statret == 1) {
sxp->st = alt_sx.st;
#ifdef SUPPORT_ACLS
if (preserve_acls && !S_ISLNK(file->mode)) {
free_acl(sxp);
if (!ACL_READY(alt_sx))
get_acl(cmpbuf, sxp);
else {
sxp->acc_acl = alt_sx.acc_acl;
sxp->def_acl = alt_sx.def_acl;
alt_sx.acc_acl = alt_sx.def_acl = NULL;
}
}
#endif
#ifdef SUPPORT_XATTRS
if (preserve_xattrs) {
free_xattr(sxp);
if (!XATTR_READY(alt_sx))
get_xattr(cmpbuf, -1, sxp);
else {
sxp->xattr = alt_sx.xattr;
alt_sx.xattr = NULL;
}
}
#endif
} else
free_stat_x(&alt_sx);
}
if (maybe_hard_link(file, ndx, fname, statret, sxp, prev_name, &prev_st,
realname, itemizing, code) < 0)
return 0;
if (remove_source_files == 1 && do_xfers)
send_msg_success(fname, ndx);
return 1;
}
int hard_link_one(struct file_struct *file, const char *fname,
const char *oldname, int terse)
{
if (do_link_at(oldname, fname) < 0) {
enum logcode code;
if (terse) {
if (!INFO_GTE(NAME, 1))
return 0;
code = FINFO;
} else
code = FERROR_XFER;
rsyserr(code, errno, "link %s => %s failed",
full_fname(fname), oldname);
return 0;
}
file->flags |= FLAG_HLINK_DONE;
return 1;
}
void finish_hard_link(struct file_struct *file, const char *fname, int fin_ndx,
STRUCT_STAT *stp, int itemizing, enum logcode code,
int alt_dest)
{
stat_x prev_sx;
STRUCT_STAT st;
char prev_name[MAXPATHLEN], alt_name[MAXPATHLEN];
const char *our_name;
struct file_list *flist;
int prev_statret, ndx, prev_ndx = F_HL_PREV(file);
if (stp == NULL && prev_ndx >= 0) {
if (link_stat(fname, &st, 0) < 0 && !dry_run) {
rsyserr(FERROR_XFER, errno, "stat %s failed",
full_fname(fname));
return;
}
stp = &st;
}
/* FIRST combined with DONE means we were the first to get done. */
file->flags |= FLAG_HLINK_FIRST | FLAG_HLINK_DONE;
F_HL_PREV(file) = alt_dest;
if (alt_dest >= 0 && dry_run) {
pathjoin(alt_name, MAXPATHLEN, basis_dir[alt_dest],
f_name(file, NULL));
our_name = alt_name;
} else
our_name = fname;
init_stat_x(&prev_sx);
while ((ndx = prev_ndx) >= 0) {
int val;
flist = flist_for_ndx(ndx, NULL);
if (!flist) {
/* The list holding an earlier member was released, which
* only happens once all of its files have completed, so
* everything from here on is already finished and there
* is nothing left to re-link. Stale indexes are cut in
* check_prior() and in the FLAG_HLINK_WAIT re-point;
* stop the walk here rather than aborting. */
break;
}
file = flist->files[ndx - flist->ndx_start];
file->flags = (file->flags & ~FLAG_HLINK_FIRST) | FLAG_HLINK_DONE;
prev_ndx = F_HL_PREV(file);
F_HL_PREV(file) = fin_ndx;
prev_statret = link_stat(f_name(file, prev_name), &prev_sx.st, 0);
val = maybe_hard_link(file, ndx, prev_name, prev_statret, &prev_sx,
our_name, stp, fname, itemizing, code);
if (file->flags & FLAG_HLINK_WAIT) {
file->flags &= ~FLAG_HLINK_WAIT;
flist->in_progress--;
}
free_stat_x(&prev_sx);
if (val < 0)
continue;
if (remove_source_files == 1 && do_xfers)
send_msg_success(fname, ndx);
}
if (inc_recurse) {
int gnum = F_HL_GNUM(file);
struct ht_int32_node *node = hashtable_find(prior_hlinks, gnum, NULL);
if (node == NULL) {
rprintf(FERROR, "Unable to find a hlink node for %d (%s)\n", gnum, f_name(file, prev_name));
exit_cleanup(RERR_MESSAGEIO);
}
if (node->data == NULL) {
rprintf(FERROR, "Hlink node data for %d is NULL (%s)\n", gnum, f_name(file, prev_name));
exit_cleanup(RERR_MESSAGEIO);
}
if (CVAL(node->data, 0) != 0) {
/* An earlier member of this group already finished and
* recorded its path as the group's root. That member
* could not be hard-linked to the original root (e.g.
* EMLINK or EXDEV), so it was written as a regular
* file: it, not the stale root, is the only inode this
* group can still link to. Re-root the group here so
* that later members link to this live file.
* (Upstream: RsyncProject/rsync#741.) */
rprintf(FINFO,
"hlink: group %d already finished at %s; re-rooting at %s (%s)\n",
gnum, (char*)node->data, our_name, f_name(file, prev_name));
}
free(node->data);
node->data = strdup(our_name);
}
}
int skip_hard_link(struct file_struct *file, struct file_list **flist_p)
{
struct file_list *flist;
int prev_ndx;
file->flags |= FLAG_SKIP_HLINK;
if (!(file->flags & FLAG_HLINK_LAST))
return -1;
check_prior(file, F_HL_GNUM(file), &prev_ndx, &flist);
if (prev_ndx >= 0) {
file = flist->files[prev_ndx - flist->ndx_start];
if (file->flags & (FLAG_HLINK_DONE|FLAG_FILE_SENT))
return -1;
file->flags |= FLAG_HLINK_LAST;
*flist_p = flist;
}
return prev_ndx;
}
#endif