Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions man/ostree-checkout.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ Boston, MA 02111-1307, USA.
</para></listitem>
</varlistentry>

<varlistentry>
<term><option>--union-add</option></term>

<listitem><para>
Keep existing directories and files.
</para></listitem>
</varlistentry>

<varlistentry>
<term><option>--allow-noent</option></term>

Expand Down
95 changes: 70 additions & 25 deletions src/libostree/ostree-repo-checkout.c
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,16 @@ checkout_file_from_input_at (OstreeRepo *self,
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (res == -1)
{
glnx_set_error_from_errno (error);
goto out;
if (errno == EEXIST && options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES)
{
ret = TRUE;
goto out;
}
else
{
glnx_set_error_from_errno (error);
goto out;
}
}

if (options->mode != OSTREE_REPO_CHECKOUT_MODE_USER)
Expand Down Expand Up @@ -240,6 +248,11 @@ checkout_file_from_input_at (OstreeRepo *self,
while (G_UNLIKELY (fd == -1 && errno == EINTR));
if (fd == -1)
{
if (errno == EEXIST && options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES)
{
ret = TRUE;
goto out;
}
glnx_set_error_from_errno (error);
goto out;
}
Expand Down Expand Up @@ -332,35 +345,45 @@ checkout_file_unioning_from_input_at (OstreeRepo *repo,
return ret;
}

typedef enum {
HARDLINK_RESULT_NOT_SUPPORTED,
HARDLINK_RESULT_SKIP_EXISTED,
HARDLINK_RESULT_LINKED
} HardlinkResult;

static gboolean
checkout_file_hardlink (OstreeRepo *self,
OstreeRepoCheckoutAtOptions *options,
const char *loose_path,
int destination_dfd,
const char *destination_name,
gboolean allow_noent,
gboolean *out_was_supported,
HardlinkResult *out_result,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
gboolean ret_was_supported = FALSE;
HardlinkResult ret_result = HARDLINK_RESULT_NOT_SUPPORTED;
int srcfd = (self->mode == OSTREE_REPO_MODE_BARE || self->mode == OSTREE_REPO_MODE_BARE_USER) ?
self->objects_dir_fd : self->uncompressed_objects_dir_fd;

again:
if (linkat (srcfd, loose_path, destination_dfd, destination_name, 0) != -1)
ret_was_supported = TRUE;
if (linkat (srcfd, loose_path, destination_dfd, destination_name, 0) == 0)
ret_result = HARDLINK_RESULT_LINKED;
else if (!options->no_copy_fallback && (errno == EMLINK || errno == EXDEV || errno == EPERM))
{
/* EMLINK, EXDEV and EPERM shouldn't be fatal; we just can't do the
* optimization of hardlinking instead of copying.
*/
ret_was_supported = FALSE;
}
else if (allow_noent && errno == ENOENT)
{
ret_was_supported = FALSE;
}
else if (errno == EEXIST && options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES)
{
/* In this mode, we keep existing content. Distinguish this case though to
* avoid inserting into the devino cache.
*/
ret_result = HARDLINK_RESULT_SKIP_EXISTED;
}
else if (errno == EEXIST && options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES)
{
Expand All @@ -370,7 +393,7 @@ checkout_file_hardlink (OstreeRepo *self,
* the same file, then rename() does nothing, and returns a
* success status."
*
* So we can't make this atomic.
* So we can't make this atomic.
*/
(void) unlinkat (destination_dfd, destination_name, 0);
goto again;
Expand All @@ -379,14 +402,12 @@ checkout_file_hardlink (OstreeRepo *self,
{
g_prefix_error (error, "Hardlinking %s to %s: ", loose_path, destination_name);
glnx_set_error_from_errno (error);
goto out;
return FALSE;
}

ret = TRUE;
if (out_was_supported)
*out_was_supported = ret_was_supported;
out:
return ret;
if (out_result)
*out_result = ret_result;
return TRUE;
}

static gboolean
Expand Down Expand Up @@ -439,7 +460,7 @@ checkout_one_file_at (OstreeRepo *repo,
}
else
{
gboolean did_hardlink = FALSE;
HardlinkResult hardlink_res = HARDLINK_RESULT_NOT_SUPPORTED;
/* Try to do a hardlink first, if it's a regular file. This also
* traverses all parent repos.
*/
Expand Down Expand Up @@ -469,11 +490,11 @@ checkout_one_file_at (OstreeRepo *repo,
options,
loose_path_buf,
destination_dfd, destination_name,
TRUE, &did_hardlink,
TRUE, &hardlink_res,
cancellable, error))
goto out;

if (did_hardlink && options->devino_to_csum_cache)
if (hardlink_res == HARDLINK_RESULT_LINKED && options->devino_to_csum_cache)
{
struct stat stbuf;
OstreeDevIno *key;
Expand All @@ -492,13 +513,13 @@ checkout_one_file_at (OstreeRepo *repo,
g_hash_table_add ((GHashTable*)options->devino_to_csum_cache, key);
}

if (did_hardlink)
if (hardlink_res != HARDLINK_RESULT_NOT_SUPPORTED)
break;
}
current_repo = current_repo->parent_repo;
}

need_copy = !did_hardlink;
need_copy = (hardlink_res == HARDLINK_RESULT_NOT_SUPPORTED);
}

can_cache = (options->enable_uncompressed_cache
Expand All @@ -514,7 +535,7 @@ checkout_one_file_at (OstreeRepo *repo,
&& repo->mode == OSTREE_REPO_MODE_ARCHIVE_Z2
&& options->mode == OSTREE_REPO_CHECKOUT_MODE_USER)
{
gboolean did_hardlink;
HardlinkResult hardlink_res = HARDLINK_RESULT_NOT_SUPPORTED;

if (!ostree_repo_load_file (repo, checksum, &input, NULL, NULL,
cancellable, error))
Expand Down Expand Up @@ -560,19 +581,20 @@ checkout_one_file_at (OstreeRepo *repo,

if (!checkout_file_hardlink (repo, options, loose_path_buf,
destination_dfd, destination_name,
FALSE, &did_hardlink,
FALSE, &hardlink_res,
cancellable, error))
{
g_prefix_error (error, "Using new cached uncompressed hardlink of %s to %s: ", checksum, destination_name);
goto out;
}

need_copy = !did_hardlink;
need_copy = (hardlink_res == HARDLINK_RESULT_NOT_SUPPORTED);
}

/* Fall back to copy if we couldn't hardlink */
if (need_copy)
{
g_assert (!options->no_copy_fallback);
if (!ostree_repo_load_file (repo, checksum, &input, NULL, &xattrs,
cancellable, error))
goto out;
Expand Down Expand Up @@ -641,6 +663,8 @@ checkout_tree_at (OstreeRepo *self,
gboolean did_exist = FALSE;
glnx_fd_close int destination_dfd = -1;
int res;
struct stat repo_dfd_stat;
struct stat destination_stat;
g_autoptr(GVariant) xattrs = NULL;
g_autoptr(GFileEnumerator) dir_enum = NULL;

Expand All @@ -653,7 +677,9 @@ checkout_tree_at (OstreeRepo *self,
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (res == -1)
{
if (errno == EEXIST && options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES)
if (errno == EEXIST &&
(options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES
|| options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES))
did_exist = TRUE;
else
{
Expand All @@ -666,6 +692,25 @@ checkout_tree_at (OstreeRepo *self,
&destination_dfd, error))
goto out;

if (fstat (self->repo_dir_fd, &repo_dfd_stat) < 0)
{
glnx_set_error_from_errno (error);
goto out;
}
if (fstat (destination_dfd, &destination_stat) < 0)
{
glnx_set_error_from_errno (error);
goto out;
}

if (options->no_copy_fallback && repo_dfd_stat.st_dev != destination_stat.st_dev)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Unable to do hardlink checkout across devices (src=%lu destination=%lu)",
repo_dfd_stat.st_dev, destination_stat.st_dev);
goto out;
}

/* Set the xattrs now, so any derived labeling works */
if (!did_exist && options->mode != OSTREE_REPO_CHECKOUT_MODE_USER)
{
Expand Down
6 changes: 4 additions & 2 deletions src/libostree/ostree-repo.h
Original file line number Diff line number Diff line change
Expand Up @@ -722,11 +722,13 @@ typedef enum {
/**
* OstreeRepoCheckoutOverwriteMode:
* @OSTREE_REPO_CHECKOUT_OVERWRITE_NONE: No special options
* @OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES: When layering checkouts, overwrite earlier files, but keep earlier directories
* @OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES: When layering checkouts, unlink() and replace existing files, but do not modify existing directories
* @OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES: Only add new files/directories
*/
typedef enum {
OSTREE_REPO_CHECKOUT_OVERWRITE_NONE = 0,
OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES = 1
OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES = 1,
OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES = 2, /* Since: 2017.3 */
} OstreeRepoCheckoutOverwriteMode;

_OSTREE_PUBLIC
Expand Down
17 changes: 14 additions & 3 deletions src/ostree/ot-builtin-checkout.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ static gboolean opt_allow_noent;
static gboolean opt_disable_cache;
static char *opt_subpath;
static gboolean opt_union;
static gboolean opt_union_add;
static gboolean opt_whiteouts;
static gboolean opt_from_stdin;
static char *opt_from_file;
Expand Down Expand Up @@ -63,6 +64,7 @@ static GOptionEntry options[] = {
{ "disable-cache", 0, 0, G_OPTION_ARG_NONE, &opt_disable_cache, "Do not update or use the internal repository uncompressed object cache", NULL },
{ "subpath", 0, 0, G_OPTION_ARG_STRING, &opt_subpath, "Checkout sub-directory PATH", "PATH" },
{ "union", 0, 0, G_OPTION_ARG_NONE, &opt_union, "Keep existing directories, overwrite existing files", NULL },
{ "union-add", 0, 0, G_OPTION_ARG_NONE, &opt_union_add, "Keep existing files/directories, only add new", NULL },
{ "whiteouts", 0, 0, G_OPTION_ARG_NONE, &opt_whiteouts, "Process 'whiteout' (Docker style) entries", NULL },
{ "allow-noent", 0, 0, G_OPTION_ARG_NONE, &opt_allow_noent, "Do nothing if specified path does not exist", NULL },
{ "from-stdin", 0, 0, G_OPTION_ARG_NONE, &opt_from_stdin, "Process many checkouts from standard input", NULL },
Expand All @@ -87,14 +89,23 @@ process_one_checkout (OstreeRepo *repo,
* `ostree_repo_checkout_at` until such time as we have a more
* convenient infrastructure for testing C APIs with data.
*/
if (opt_disable_cache || opt_whiteouts || opt_require_hardlinks)
if (opt_disable_cache || opt_whiteouts || opt_require_hardlinks || opt_union_add)
{
OstreeRepoCheckoutAtOptions options = { 0, };

if (opt_user_mode)
options.mode = OSTREE_REPO_CHECKOUT_MODE_USER;
if (opt_union)
/* Can't union these */
if (opt_union && opt_union_add)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Cannot specify both --union and --union-add");
goto out;
}
else if (opt_union)
options.overwrite_mode = OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES;
else if (opt_union_add)
options.overwrite_mode = OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES;
if (opt_whiteouts)
options.process_whiteouts = TRUE;
if (subpath)
Expand Down
20 changes: 19 additions & 1 deletion tests/basic-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

set -euo pipefail

echo "1..62"
echo "1..63"

$CMD_PREFIX ostree --version > version.yaml
python -c 'import yaml; yaml.safe_load(open("version.yaml"))'
Expand Down Expand Up @@ -279,6 +279,24 @@ cd checkout-test2-union
assert_file_has_content ./yet/another/tree/green "leaf"
echo "ok checkout union 1"

cd ${test_tmpdir}
$OSTREE commit -b test-union-add --tree=ref=test2
$OSTREE checkout test-union-add checkout-test-union-add
echo 'file for union add testing' > checkout-test-union-add/union-add-test
echo 'another file for union add testing' > checkout-test-union-add/union-add-test2
$OSTREE commit -b test-union-add --tree=dir=checkout-test-union-add
rm checkout-test-union-add -rf
# Check out previous
$OSTREE checkout test-union-add^ checkout-test-union-add
assert_not_has_file checkout-test-union-add/union-add-test
assert_not_has_file checkout-test-union-add/union-add-test2
# Now create a file we don't want overwritten
echo 'existing file for union add' > checkout-test-union-add/union-add-test
$OSTREE checkout --union-add test-union-add checkout-test-union-add
assert_file_has_content checkout-test-union-add/union-add-test 'existing file for union add'
assert_file_has_content checkout-test-union-add/union-add-test2 'another file for union add testing'
echo "ok checkout union add"

cd ${test_tmpdir}
rm -rf shadow-repo
mkdir shadow-repo
Expand Down
2 changes: 1 addition & 1 deletion tests/test-rofiles-fuse.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ assert_file_has_content mnt/test2-checkout-copy-fallback/anewfile-for-fuse anewf
if ${CMD_PREFIX} ostree --repo=repo checkout -UH test2 mnt/test2-checkout-copy-hardlinked 2>err.txt; then
assert_not_reached "Checking out via hardlinks across mountpoint unexpectedly succeeded!"
fi
assert_file_has_content err.txt "Invalid cross-device link"
assert_file_has_content err.txt "Unable to do hardlink checkout across devices"

echo "ok checkout copy fallback"