ostree/src/libostree/ostree-repo-checkout.c

1128 lines
39 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
*
* Copyright (C) 2011,2013 Colin Walters <walters@verbum.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*
* Author: Colin Walters <walters@verbum.org>
*/
#include "config.h"
#include <glib-unix.h>
#include <sys/xattr.h>
#include <gio/gfiledescriptorbased.h>
#include <gio/gunixoutputstream.h>
#include "otutil.h"
#include "ostree-repo-file.h"
#include "ostree-core-private.h"
#include "ostree-repo-private.h"
#define WHITEOUT_PREFIX ".wh."
static gboolean
checkout_object_for_uncompressed_cache (OstreeRepo *self,
const char *loose_path,
GFileInfo *src_info,
GInputStream *content,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
g_autofree char *temp_filename = NULL;
g_autoptr(GOutputStream) temp_out = NULL;
glnx_fd_close int fd = -1;
int res;
guint32 file_mode;
/* Don't make setuid files in uncompressed cache */
file_mode = g_file_info_get_attribute_uint32 (src_info, "unix::mode");
file_mode &= ~(S_ISUID|S_ISGID);
if (!glnx_open_tmpfile_linkable_at (self->tmp_dir_fd, ".", O_WRONLY | O_CLOEXEC,
&fd, &temp_filename,
error))
goto out;
temp_out = g_unix_output_stream_new (fd, FALSE);
if (g_output_stream_splice (temp_out, content, 0, cancellable, error) < 0)
goto out;
if (!g_output_stream_flush (temp_out, cancellable, error))
goto out;
if (!self->disable_fsync)
{
do
res = fsync (fd);
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (G_UNLIKELY (res == -1))
{
glnx_set_error_from_errno (error);
goto out;
}
}
if (!g_output_stream_close (temp_out, cancellable, error))
goto out;
if (fchmod (fd, file_mode) < 0)
{
glnx_set_error_from_errno (error);
goto out;
}
if (!_ostree_repo_ensure_loose_objdir_at (self->uncompressed_objects_dir_fd,
loose_path,
cancellable, error))
goto out;
if (!glnx_link_tmpfile_at (self->tmp_dir_fd, GLNX_LINK_TMPFILE_NOREPLACE_IGNORE_EXIST,
fd, temp_filename,
self->uncompressed_objects_dir_fd, loose_path,
error))
goto out;
ret = TRUE;
out:
return ret;
}
static gboolean
fsync_is_enabled (OstreeRepo *self,
OstreeRepoCheckoutAtOptions *options)
{
return options->enable_fsync;
}
static gboolean
write_regular_file_content (OstreeRepo *self,
OstreeRepoCheckoutAtOptions *options,
GOutputStream *output,
GFileInfo *file_info,
GVariant *xattrs,
GInputStream *input,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
const OstreeRepoCheckoutMode mode = options->mode;
int fd;
int res;
if (g_output_stream_splice (output, input, 0,
cancellable, error) < 0)
goto out;
if (!g_output_stream_flush (output, cancellable, error))
goto out;
fd = g_file_descriptor_based_get_fd ((GFileDescriptorBased*)output);
if (mode != OSTREE_REPO_CHECKOUT_MODE_USER)
{
do
res = fchown (fd,
g_file_info_get_attribute_uint32 (file_info, "unix::uid"),
g_file_info_get_attribute_uint32 (file_info, "unix::gid"));
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (G_UNLIKELY (res == -1))
{
glnx_set_error_from_errno (error);
goto out;
}
do
res = fchmod (fd, g_file_info_get_attribute_uint32 (file_info, "unix::mode"));
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (G_UNLIKELY (res == -1))
{
glnx_set_error_from_errno (error);
goto out;
}
if (xattrs)
{
if (!glnx_fd_set_all_xattrs (fd, xattrs, cancellable, error))
goto out;
}
}
if (fsync_is_enabled (self, options))
{
if (fsync (fd) == -1)
{
glnx_set_error_from_errno (error);
goto out;
}
}
if (!g_output_stream_close (output, cancellable, error))
goto out;
ret = TRUE;
out:
return ret;
}
static gboolean
checkout_file_from_input_at (OstreeRepo *self,
OstreeRepoCheckoutAtOptions *options,
GFileInfo *file_info,
GVariant *xattrs,
GInputStream *input,
int destination_dfd,
const char *destination_name,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
int res;
if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_SYMBOLIC_LINK)
{
do
res = symlinkat (g_file_info_get_symlink_target (file_info),
destination_dfd, destination_name);
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (res == -1)
{
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)
{
if (G_UNLIKELY (fchownat (destination_dfd, destination_name,
g_file_info_get_attribute_uint32 (file_info, "unix::uid"),
g_file_info_get_attribute_uint32 (file_info, "unix::gid"),
AT_SYMLINK_NOFOLLOW) == -1))
{
glnx_set_error_from_errno (error);
goto out;
}
if (xattrs)
{
if (!glnx_dfd_name_set_all_xattrs (destination_dfd, destination_name,
xattrs, cancellable, error))
goto out;
}
}
}
else if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_REGULAR)
{
g_autoptr(GOutputStream) temp_out = NULL;
int fd;
guint32 file_mode;
file_mode = g_file_info_get_attribute_uint32 (file_info, "unix::mode");
/* Don't make setuid files on checkout when we're doing --user */
if (options->mode == OSTREE_REPO_CHECKOUT_MODE_USER)
file_mode &= ~(S_ISUID|S_ISGID);
do
fd = openat (destination_dfd, destination_name, O_WRONLY | O_CREAT | O_EXCL, file_mode);
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;
}
temp_out = g_unix_output_stream_new (fd, TRUE);
fd = -1; /* Transfer ownership */
if (!write_regular_file_content (self, options, temp_out, file_info, xattrs, input,
cancellable, error))
goto out;
}
else
g_assert_not_reached ();
ret = TRUE;
out:
return ret;
}
/*
* This function creates a file under a temporary name, then rename()s
* it into place. This implements union-like behavior.
*/
static gboolean
checkout_file_unioning_from_input_at (OstreeRepo *repo,
OstreeRepoCheckoutAtOptions *options,
GFileInfo *file_info,
GVariant *xattrs,
GInputStream *input,
int destination_dfd,
const char *destination_name,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
g_autofree char *temp_filename = NULL;
if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_SYMBOLIC_LINK)
{
if (!_ostree_make_temporary_symlink_at (destination_dfd,
g_file_info_get_symlink_target (file_info),
&temp_filename,
cancellable, error))
goto out;
if (xattrs)
{
if (!glnx_dfd_name_set_all_xattrs (destination_dfd, temp_filename,
xattrs, cancellable, error))
goto out;
}
if (G_UNLIKELY (renameat (destination_dfd, temp_filename,
destination_dfd, destination_name) == -1))
{
glnx_set_error_from_errno (error);
goto out;
}
}
else if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_REGULAR)
{
glnx_fd_close int temp_fd = -1;
g_autoptr(GOutputStream) temp_out = NULL;
guint32 file_mode;
file_mode = g_file_info_get_attribute_uint32 (file_info, "unix::mode");
/* Don't make setuid files on checkout when we're doing --user */
if (options->mode == OSTREE_REPO_CHECKOUT_MODE_USER)
file_mode &= ~(S_ISUID|S_ISGID);
if (!glnx_open_tmpfile_linkable_at (destination_dfd, ".", O_WRONLY | O_CLOEXEC,
&temp_fd, &temp_filename,
error))
goto out;
temp_out = g_unix_output_stream_new (temp_fd, FALSE);
if (!write_regular_file_content (repo, options, temp_out, file_info, xattrs, input,
cancellable, error))
goto out;
if (!glnx_link_tmpfile_at (destination_dfd, GLNX_LINK_TMPFILE_REPLACE,
temp_fd, temp_filename, destination_dfd,
destination_name,
error))
goto out;
}
else
g_assert_not_reached ();
ret = TRUE;
out:
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,
HardlinkResult *out_result,
GCancellable *cancellable,
GError **error)
{
HardlinkResult ret_result = HARDLINK_RESULT_NOT_SUPPORTED;
int srcfd = _ostree_repo_mode_is_bare (self->mode) ?
self->objects_dir_fd : self->uncompressed_objects_dir_fd;
again:
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.
*/
}
else if (allow_noent && errno == ENOENT)
{
}
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)
{
/* Idiocy, from man rename(2)
*
* "If oldpath and newpath are existing hard links referring to
* the same file, then rename() does nothing, and returns a
* success status."
*
* So we can't make this atomic.
*/
(void) unlinkat (destination_dfd, destination_name, 0);
goto again;
}
else
{
g_prefix_error (error, "Hardlinking %s to %s: ", loose_path, destination_name);
glnx_set_error_from_errno (error);
return FALSE;
}
if (out_result)
*out_result = ret_result;
return TRUE;
}
static gboolean
checkout_one_file_at (OstreeRepo *repo,
OstreeRepoCheckoutAtOptions *options,
GFile *source,
GFileInfo *source_info,
int destination_dfd,
const char *destination_name,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
const char *checksum;
gboolean is_symlink;
gboolean is_bare_user_symlink = FALSE;
gboolean can_cache;
gboolean need_copy = TRUE;
char loose_path_buf[_OSTREE_LOOSE_PATH_MAX];
g_autoptr(GInputStream) input = NULL;
g_autoptr(GVariant) xattrs = NULL;
gboolean is_whiteout;
is_symlink = g_file_info_get_file_type (source_info) == G_FILE_TYPE_SYMBOLIC_LINK;
checksum = ostree_repo_file_get_checksum ((OstreeRepoFile*)source);
is_whiteout = !is_symlink && options->process_whiteouts &&
g_str_has_prefix (destination_name, WHITEOUT_PREFIX);
/* First, see if it's a Docker whiteout,
* https://github.com/docker/docker/blob/1a714e76a2cb9008cd19609059e9988ff1660b78/pkg/archive/whiteouts.go
*/
if (is_whiteout)
{
const char *name = destination_name + (sizeof (WHITEOUT_PREFIX) - 1);
if (!name[0])
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Invalid empty whiteout '%s'", name);
goto out;
}
g_assert (name[0] != '/'); /* Sanity */
if (!glnx_shutil_rm_rf_at (destination_dfd, name, cancellable, error))
goto out;
need_copy = FALSE;
}
else
{
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.
*/
OstreeRepo *current_repo = repo;
while (current_repo)
{
/* TODO - Hoist this up to the toplevel at least for checking out from
* !parent; don't need to compute it for each file.
*/
gboolean repo_is_usermode =
current_repo->mode == OSTREE_REPO_MODE_BARE_USER ||
current_repo->mode == OSTREE_REPO_MODE_BARE_USER_ONLY;
/* We're hardlinkable if the checkout mode matches the repo mode */
gboolean is_hardlinkable =
(current_repo->mode == OSTREE_REPO_MODE_BARE
&& options->mode == OSTREE_REPO_CHECKOUT_MODE_NONE) ||
(repo_is_usermode && options->mode == OSTREE_REPO_CHECKOUT_MODE_USER);
gboolean current_can_cache = (options->enable_uncompressed_cache
&& current_repo->enable_uncompressed_cache);
gboolean is_archive_z2_with_cache = (current_repo->mode == OSTREE_REPO_MODE_ARCHIVE_Z2
&& options->mode == OSTREE_REPO_CHECKOUT_MODE_USER
&& current_can_cache);
/* NOTE: bare-user symlinks are not stored as symlinks; see
* https://github.com/ostreedev/ostree/commit/47c612e5a0688c3452a125655a245e8f4f01b2b0
* as well as write_object().
*/
is_bare_user_symlink = (repo_is_usermode && is_symlink);
const gboolean is_bare = is_hardlinkable && !is_bare_user_symlink;
/* Verify if no_copy_fallback is set that we can hardlink, with a
* special exception for bare-user symlinks.
*/
if (options->no_copy_fallback && !is_hardlinkable && !is_bare_user_symlink)
{
glnx_throw (error,
repo_is_usermode ?
"User repository mode requires user checkout mode to hardlink" :
"Bare repository mode cannot hardlink in user checkout mode");
goto out;
}
/* But only under these conditions */
if (is_bare || is_archive_z2_with_cache)
{
/* Override repo mode; for archive-z2 we're looking in
the cache, which is in "bare" form */
_ostree_loose_path (loose_path_buf, checksum, OSTREE_OBJECT_TYPE_FILE, OSTREE_REPO_MODE_BARE);
if (!checkout_file_hardlink (current_repo,
options,
loose_path_buf,
destination_dfd, destination_name,
TRUE, &hardlink_res,
cancellable, error))
goto out;
if (hardlink_res == HARDLINK_RESULT_LINKED && options->devino_to_csum_cache)
{
struct stat stbuf;
OstreeDevIno *key;
if (TEMP_FAILURE_RETRY (fstatat (destination_dfd, destination_name, &stbuf, AT_SYMLINK_NOFOLLOW)) != 0)
{
glnx_set_error_from_errno (error);
goto out;
}
key = g_new (OstreeDevIno, 1);
key->dev = stbuf.st_dev;
key->ino = stbuf.st_ino;
memcpy (key->checksum, checksum, OSTREE_SHA256_STRING_LEN+1);
g_hash_table_add ((GHashTable*)options->devino_to_csum_cache, key);
}
if (hardlink_res != HARDLINK_RESULT_NOT_SUPPORTED)
break;
}
current_repo = current_repo->parent_repo;
}
need_copy = (hardlink_res == HARDLINK_RESULT_NOT_SUPPORTED);
}
can_cache = (options->enable_uncompressed_cache
&& repo->enable_uncompressed_cache);
/* Ok, if we're archive-z2 and we didn't find an object, uncompress
* it now, stick it in the cache, and then hardlink to that.
*/
if (can_cache
&& !is_whiteout
&& !is_symlink
&& need_copy
&& repo->mode == OSTREE_REPO_MODE_ARCHIVE_Z2
&& options->mode == OSTREE_REPO_CHECKOUT_MODE_USER)
{
HardlinkResult hardlink_res = HARDLINK_RESULT_NOT_SUPPORTED;
if (!ostree_repo_load_file (repo, checksum, &input, NULL, NULL,
cancellable, error))
goto out;
/* Overwrite any parent repo from earlier */
_ostree_loose_path (loose_path_buf, checksum, OSTREE_OBJECT_TYPE_FILE, OSTREE_REPO_MODE_BARE);
if (!checkout_object_for_uncompressed_cache (repo, loose_path_buf,
source_info, input,
cancellable, error))
{
g_prefix_error (error, "Unpacking loose object %s: ", checksum);
goto out;
}
g_clear_object (&input);
/* Store the 2-byte objdir prefix (e.g. e3) in a set. The basic
* idea here is that if we had to unpack an object, it's very
* likely we're replacing some other object, so we may need a GC.
*
* This model ensures that we do work roughly proportional to
* the size of the changes. For example, we don't scan any
* directories if we didn't modify anything, meaning you can
* checkout the same tree multiple times very quickly.
*
* This is also scale independent; we don't hardcode e.g. looking
* at 1000 objects.
*
* The downside is that if we're unlucky, we may not free
* an object for quite some time.
*/
g_mutex_lock (&repo->cache_lock);
{
gpointer key = GUINT_TO_POINTER ((g_ascii_xdigit_value (checksum[0]) << 4) +
g_ascii_xdigit_value (checksum[1]));
if (repo->updated_uncompressed_dirs == NULL)
repo->updated_uncompressed_dirs = g_hash_table_new (NULL, NULL);
g_hash_table_add (repo->updated_uncompressed_dirs, key);
}
g_mutex_unlock (&repo->cache_lock);
if (!checkout_file_hardlink (repo, options, loose_path_buf,
destination_dfd, destination_name,
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 = (hardlink_res == HARDLINK_RESULT_NOT_SUPPORTED);
}
/* Fall back to copy if we couldn't hardlink */
if (need_copy)
{
/* Bare user mode can't hardlink symlinks, so we need to do a copy for
* those. (Although in the future we could hardlink inside checkouts) This
* assertion is intended to ensure that for regular files at least, we
* succeeded at hardlinking above.
*/
if (options->no_copy_fallback)
g_assert (is_bare_user_symlink);
if (!ostree_repo_load_file (repo, checksum, &input, NULL, &xattrs,
cancellable, error))
goto out;
if (options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES)
{
if (!checkout_file_unioning_from_input_at (repo, options, source_info, xattrs, input,
destination_dfd,
destination_name,
cancellable, error))
{
g_prefix_error (error, "Union checkout of %s to %s: ", checksum, destination_name);
goto out;
}
}
else
{
if (!checkout_file_from_input_at (repo, options, source_info, xattrs, input,
destination_dfd,
destination_name,
cancellable, error))
{
g_prefix_error (error, "Checkout of %s to %s: ", checksum, destination_name);
goto out;
}
}
if (input)
{
if (!g_input_stream_close (input, cancellable, error))
goto out;
}
}
ret = TRUE;
out:
return ret;
}
/*
* checkout_tree_at:
* @self: Repo
* @mode: Options controlling all files
* @overwrite_mode: Whether or not to overwrite files
* @destination_parent_fd: Place tree here
* @destination_name: Use this name for tree
* @source: Source tree
* @source_info: Source info
* @cancellable: Cancellable
* @error: Error
*
* Like ostree_repo_checkout_tree(), but check out @source into the
* relative @destination_name, located by @destination_parent_fd.
*/
static gboolean
checkout_tree_at (OstreeRepo *self,
OstreeRepoCheckoutAtOptions *options,
int destination_parent_fd,
const char *destination_name,
OstreeRepoFile *source,
GFileInfo *source_info,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
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;
/* Create initially with mode 0700, then chown/chmod only when we're
* done. This avoids anyone else being able to operate on partially
* constructed dirs.
*/
do
res = mkdirat (destination_parent_fd, destination_name, 0700);
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (res == -1)
{
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
{
glnx_set_error_from_errno (error);
goto out;
}
}
if (!glnx_opendirat (destination_parent_fd, destination_name, TRUE,
&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=%"G_GUINT64_FORMAT" destination=%"G_GUINT64_FORMAT")",
(guint64)repo_dfd_stat.st_dev, (guint64)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)
{
if (!ostree_repo_file_get_xattrs (source, &xattrs, NULL, error))
goto out;
if (xattrs)
{
if (!glnx_fd_set_all_xattrs (destination_dfd, xattrs, cancellable, error))
goto out;
}
}
if (g_file_info_get_file_type (source_info) != G_FILE_TYPE_DIRECTORY)
{
ret = checkout_one_file_at (self, options,
(GFile *) source,
source_info,
destination_dfd,
g_file_info_get_name (source_info),
cancellable, error);
goto out;
}
dir_enum = g_file_enumerate_children ((GFile*)source,
OSTREE_GIO_FAST_QUERYINFO,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
cancellable,
error);
if (!dir_enum)
goto out;
while (TRUE)
{
GFileInfo *file_info;
GFile *src_child;
const char *name;
if (!g_file_enumerator_iterate (dir_enum, &file_info, &src_child,
cancellable, error))
goto out;
if (file_info == NULL)
break;
name = g_file_info_get_name (file_info);
if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY)
{
if (!checkout_tree_at (self, options,
destination_dfd, name,
(OstreeRepoFile*)src_child, file_info,
cancellable, error))
goto out;
}
else
{
if (!checkout_one_file_at (self, options,
src_child, file_info,
destination_dfd, name,
cancellable, error))
goto out;
}
}
/* We do fchmod/fchown last so that no one else could access the
* partially created directory and change content we're laying out.
*/
if (!did_exist)
{
do
res = fchmod (destination_dfd,
g_file_info_get_attribute_uint32 (source_info, "unix::mode"));
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (G_UNLIKELY (res == -1))
{
glnx_set_error_from_errno (error);
goto out;
}
}
if (!did_exist && options->mode != OSTREE_REPO_CHECKOUT_MODE_USER)
{
do
res = fchown (destination_dfd,
g_file_info_get_attribute_uint32 (source_info, "unix::uid"),
g_file_info_get_attribute_uint32 (source_info, "unix::gid"));
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (G_UNLIKELY (res == -1))
{
glnx_set_error_from_errno (error);
goto out;
}
}
/* Set directory mtime to OSTREE_TIMESTAMP, so that it is constant for all checkouts.
* Must be done after setting permissions and creating all children.
*/
if (!did_exist)
{
const struct timespec times[2] = { { OSTREE_TIMESTAMP, UTIME_OMIT }, { OSTREE_TIMESTAMP, 0} };
do
res = futimens (destination_dfd, times);
while (G_UNLIKELY (res == -1 && errno == EINTR));
if (G_UNLIKELY (res == -1))
{
glnx_set_error_from_errno (error);
goto out;
}
}
if (fsync_is_enabled (self, options))
{
if (fsync (destination_dfd) == -1)
{
glnx_set_error_from_errno (error);
goto out;
}
}
ret = TRUE;
out:
return ret;
}
/**
* ostree_repo_checkout_tree:
* @self: Repo
* @mode: Options controlling all files
* @overwrite_mode: Whether or not to overwrite files
* @destination: Place tree here
* @source: Source tree
* @source_info: Source info
* @cancellable: Cancellable
* @error: Error
*
* Check out @source into @destination, which must live on the
* physical filesystem. @source may be any subdirectory of a given
* commit. The @mode and @overwrite_mode allow control over how the
* files are checked out.
*/
gboolean
ostree_repo_checkout_tree (OstreeRepo *self,
OstreeRepoCheckoutMode mode,
OstreeRepoCheckoutOverwriteMode overwrite_mode,
GFile *destination,
OstreeRepoFile *source,
GFileInfo *source_info,
GCancellable *cancellable,
GError **error)
{
OstreeRepoCheckoutAtOptions options = { 0, };
if (ostree_repo_get_mode (self) == OSTREE_REPO_MODE_BARE_USER_ONLY)
mode = OSTREE_REPO_CHECKOUT_MODE_USER;
options.mode = mode;
options.overwrite_mode = overwrite_mode;
/* Backwards compatibility */
options.enable_uncompressed_cache = TRUE;
return checkout_tree_at (self, &options,
AT_FDCWD, gs_file_get_path_cached (destination),
source, source_info,
cancellable, error);
}
/**
* ostree_repo_checkout_tree_at: (skip)
* @self: Repo
* @options: (allow-none): Options
* @destination_dfd: Directory FD for destination
* @destination_path: Directory for destination
* @commit: Checksum for commit
* @cancellable: Cancellable
* @error: Error
*
* Similar to ostree_repo_checkout_tree(), but uses directory-relative
* paths for the destination, uses a new `OstreeRepoCheckoutAtOptions`,
* and takes a commit checksum and optional subpath pair, rather than
* requiring use of `GFile` APIs for the caller.
*
* Note in addition that unlike ostree_repo_checkout_tree(), the
* default is not to use the repository-internal uncompressed objects
* cache.
*
* This function is deprecated. Use ostree_repo_checkout_at() instead.
*/
gboolean
ostree_repo_checkout_tree_at (OstreeRepo *self,
OstreeRepoCheckoutOptions *options,
int destination_dfd,
const char *destination_path,
const char *commit,
GCancellable *cancellable,
GError **error)
{
OstreeRepoCheckoutAtOptions new_opts = {0, };
new_opts.mode = options->mode;
new_opts.overwrite_mode = options->overwrite_mode;
new_opts.enable_uncompressed_cache = options->enable_uncompressed_cache;
new_opts.enable_fsync = options->disable_fsync ? FALSE : self->disable_fsync;
new_opts.process_whiteouts = options->process_whiteouts;
new_opts.no_copy_fallback = options->no_copy_fallback;
new_opts.subpath = options->subpath;
new_opts.devino_to_csum_cache = options->devino_to_csum_cache;
return ostree_repo_checkout_at (self, &new_opts, destination_dfd,
destination_path, commit, cancellable, error);
}
/**
* ostree_repo_checkout_at:
* @self: Repo
* @options: (allow-none): Options
* @destination_dfd: Directory FD for destination
* @destination_path: Directory for destination
* @commit: Checksum for commit
* @cancellable: Cancellable
* @error: Error
*
* Similar to ostree_repo_checkout_tree(), but uses directory-relative
* paths for the destination, uses a new `OstreeRepoCheckoutAtOptions`,
* and takes a commit checksum and optional subpath pair, rather than
* requiring use of `GFile` APIs for the caller.
*
* It also replaces ostree_repo_checkout_at() which was not safe to
* use with GObject introspection.
*
* Note in addition that unlike ostree_repo_checkout_tree(), the
* default is not to use the repository-internal uncompressed objects
* cache.
*/
gboolean
ostree_repo_checkout_at (OstreeRepo *self,
OstreeRepoCheckoutAtOptions *options,
int destination_dfd,
const char *destination_path,
const char *commit,
GCancellable *cancellable,
GError **error)
{
OstreeRepoCheckoutAtOptions default_options = { 0, };
OstreeRepoCheckoutAtOptions real_options;
if (!options)
{
default_options.subpath = NULL;
options = &default_options;
}
/* Make a copy so we can modify the options */
real_options = *options;
options = &real_options;
if (ostree_repo_get_mode (self) == OSTREE_REPO_MODE_BARE_USER_ONLY)
options->mode = OSTREE_REPO_CHECKOUT_MODE_USER;
g_autoptr(GFile) commit_root = (GFile*) _ostree_repo_file_new_for_commit (self, commit, error);
if (!commit_root)
return FALSE;
if (!ostree_repo_file_ensure_resolved ((OstreeRepoFile*)commit_root, error))
return FALSE;
g_autoptr(GFile) target_dir = NULL;
if (options->subpath && strcmp (options->subpath, "/") != 0)
target_dir = g_file_get_child (commit_root, options->subpath);
else
target_dir = g_object_ref (commit_root);
g_autoptr(GFileInfo) target_info =
g_file_query_info (target_dir, OSTREE_GIO_FAST_QUERYINFO,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
cancellable, error);
if (!target_info)
return FALSE;
if (!checkout_tree_at (self, options,
destination_dfd,
destination_path,
(OstreeRepoFile*)target_dir, target_info,
cancellable, error))
return FALSE;
return TRUE;
}
static guint
devino_hash (gconstpointer a)
{
OstreeDevIno *a_i = (gpointer)a;
return (guint) (a_i->dev + a_i->ino);
}
static int
devino_equal (gconstpointer a,
gconstpointer b)
{
OstreeDevIno *a_i = (gpointer)a;
OstreeDevIno *b_i = (gpointer)b;
return a_i->dev == b_i->dev
&& a_i->ino == b_i->ino;
}
/**
* ostree_repo_devino_cache_new:
*
* OSTree has support for pairing ostree_repo_checkout_tree_at() using
* hardlinks in combination with a later
* ostree_repo_write_directory_to_mtree() using a (normally modified)
* directory. In order for OSTree to optimally detect just the new
* files, use this function and fill in the `devino_to_csum_cache`
* member of `OstreeRepoCheckoutAtOptions`, then call
* ostree_repo_commit_set_devino_cache().
*
* Returns: (transfer full): Newly allocated cache
*/
OstreeRepoDevInoCache *
ostree_repo_devino_cache_new (void)
{
return (OstreeRepoDevInoCache*) g_hash_table_new_full (devino_hash, devino_equal, g_free, NULL);
}
/**
* ostree_repo_checkout_gc:
* @self: Repo
* @cancellable: Cancellable
* @error: Error
*
* Call this after finishing a succession of checkout operations; it
* will delete any currently-unused uncompressed objects from the
* cache.
*/
gboolean
ostree_repo_checkout_gc (OstreeRepo *self,
GCancellable *cancellable,
GError **error)
{
g_autoptr(GHashTable) to_clean_dirs = NULL;
g_mutex_lock (&self->cache_lock);
to_clean_dirs = self->updated_uncompressed_dirs;
self->updated_uncompressed_dirs = g_hash_table_new (NULL, NULL);
g_mutex_unlock (&self->cache_lock);
GHashTableIter iter;
gpointer key, value;
if (to_clean_dirs)
g_hash_table_iter_init (&iter, to_clean_dirs);
while (to_clean_dirs && g_hash_table_iter_next (&iter, &key, &value))
{
g_autofree char *objdir_name = g_strdup_printf ("%02x", GPOINTER_TO_UINT (key));
g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
if (!glnx_dirfd_iterator_init_at (self->uncompressed_objects_dir_fd, objdir_name, FALSE,
&dfd_iter, error))
return FALSE;
while (TRUE)
{
struct dirent *dent;
struct stat stbuf;
if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error))
return FALSE;
if (dent == NULL)
break;
if (fstatat (dfd_iter.fd, dent->d_name, &stbuf, AT_SYMLINK_NOFOLLOW) != 0)
{
glnx_set_error_from_errno (error);
return FALSE;
}
if (stbuf.st_nlink == 1)
{
if (unlinkat (dfd_iter.fd, dent->d_name, 0) != 0)
{
glnx_set_error_from_errno (error);
return FALSE;
}
}
}
}
return TRUE;
}