Skip to content
Snippets Groups Projects
Commit b8db8e2e authored by Simon McVittie's avatar Simon McVittie
Browse files

Merge branch 'wip/smcv/hardlink-copy' into 'master'

utils: Add a method to copy a directory tree using hard links

See merge request steam/pressure-vessel!30
parents 95857cc4 181337cd
Branches
Tags
No related merge requests found
...@@ -136,6 +136,7 @@ scripts = [ ...@@ -136,6 +136,7 @@ scripts = [
] ]
tests = [ tests = [
'cheap-copy.py',
'mypy.sh', 'mypy.sh',
'pycodestyle.sh', 'pycodestyle.sh',
'pyflakes.sh', 'pyflakes.sh',
...@@ -149,10 +150,16 @@ test_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) ...@@ -149,10 +150,16 @@ test_env.set('G_TEST_BUILDDIR', meson.current_build_dir())
test_env.set('PRESSURE_VESSEL_UNINSTALLED', 'yes') test_env.set('PRESSURE_VESSEL_UNINSTALLED', 'yes')
foreach test_name : tests foreach test_name : tests
test_args = ['-v', files('tests/' + test_name)]
if test_name.endswith('.py')
test_args += ['-e', python.path()]
endif
if prove.found() if prove.found()
test( test(
test_name, prove, test_name, prove,
args : ['-v', files('tests/' + test_name)], args : test_args,
env : test_env, env : test_env,
) )
endif endif
...@@ -276,6 +283,28 @@ executable( ...@@ -276,6 +283,28 @@ executable(
install_dir : get_option('bindir'), install_dir : get_option('bindir'),
) )
executable(
'test-cheap-copy',
sources : [
'src/flatpak-utils-base.c',
'src/flatpak-utils-base-private.h',
'src/glib-backports.c',
'src/glib-backports.h',
'src/utils.c',
'src/utils.h',
'tests/cheap-copy.c',
],
c_args : [
'-Wno-unused-local-typedefs',
],
dependencies : [
dependency('gio-unix-2.0', required : true),
subproject('libglnx').get_variable('libglnx_dep'),
],
include_directories : project_include_dirs,
install : false,
)
if get_option('man') if get_option('man')
pandoc = find_program('pandoc', required : true) pandoc = find_program('pandoc', required : true)
......
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
#include "utils.h" #include "utils.h"
#include <ftw.h>
#include <glib.h> #include <glib.h>
#include <glib/gstdio.h> #include <glib/gstdio.h>
#include <gio/gio.h> #include <gio/gio.h>
...@@ -250,3 +252,124 @@ pv_hash_table_get_arbitrary_key (GHashTable *table) ...@@ -250,3 +252,124 @@ pv_hash_table_get_arbitrary_key (GHashTable *table)
else else
return NULL; return NULL;
} }
/* nftw() doesn't have a user_data argument so we need to use a global
* variable :-( */
static struct
{
gchar *source_root;
gchar *dest_root;
GError *error;
} nftw_data;
static int
copy_tree_helper (const char *fpath,
const struct stat *sb,
int typeflag,
struct FTW *ftwbuf)
{
size_t len;
const char *suffix;
g_autofree gchar *dest = NULL;
g_autofree gchar *target = NULL;
GError **error = &nftw_data.error;
g_return_val_if_fail (g_str_has_prefix (fpath, nftw_data.source_root), 1);
if (strcmp (fpath, nftw_data.source_root) == 0)
{
if (typeflag != FTW_D)
{
glnx_throw (error, "\"%s\" is not a directory", fpath);
return 1;
}
if (!glnx_shutil_mkdir_p_at (-1, nftw_data.dest_root, 0700, NULL,
error))
return 1;
return 0;
}
len = strlen (nftw_data.source_root);
g_return_val_if_fail (fpath[len] == '/', 1);
suffix = &fpath[len + 1];
dest = g_build_filename (nftw_data.dest_root, suffix, NULL);
switch (typeflag)
{
case FTW_D:
/* For now we assume the permissions are not significant */
if (!glnx_shutil_mkdir_p_at (-1, dest, 0755, NULL, error))
return 1;
break;
case FTW_SL:
target = glnx_readlinkat_malloc (-1, fpath, NULL, error);
if (target == NULL)
return 1;
if (symlink (target, dest) != 0)
{
glnx_throw_errno_prefix (error,
"Unable to create symlink at \"%s\"",
dest);
return 1;
}
break;
case FTW_F:
/* TODO: If creating a hard link doesn't work, fall back to
* copying */
if (link (fpath, dest) != 0)
{
glnx_throw_errno_prefix (error,
"Unable to create hard link from \"%s\" to \"%s\"",
fpath, dest);
return 1;
}
break;
default:
glnx_throw (&nftw_data.error,
"Don't know how to handle ftw type flag %d at %s",
typeflag, fpath);
return 1;
}
return 0;
}
gboolean
pv_cheap_tree_copy (const char *source_root,
const char *dest_root,
GError **error)
{
int res;
/* Can't run concurrently */
g_return_val_if_fail (nftw_data.source_root == NULL, FALSE);
nftw_data.source_root = flatpak_canonicalize_filename (source_root);
nftw_data.dest_root = flatpak_canonicalize_filename (dest_root);
nftw_data.error = NULL;
res = nftw (nftw_data.source_root, copy_tree_helper, 100, FTW_PHYS);
if (res == -1)
{
g_assert (nftw_data.error == NULL);
glnx_throw_errno_prefix (error, "Unable to copy \"%s\" to \"%s\"",
source_root, dest_root);
}
else if (res != 0)
{
g_propagate_error (error, g_steal_pointer (&nftw_data.error));
}
g_clear_pointer (&nftw_data.source_root, g_free);
g_clear_pointer (&nftw_data.dest_root, g_free);
g_assert (nftw_data.error == NULL);
return (res == 0);
}
...@@ -42,3 +42,7 @@ gchar *pv_capture_output (const char * const * argv, ...@@ -42,3 +42,7 @@ gchar *pv_capture_output (const char * const * argv,
GError **error); GError **error);
gpointer pv_hash_table_get_arbitrary_key (GHashTable *table); gpointer pv_hash_table_get_arbitrary_key (GHashTable *table);
gboolean pv_cheap_tree_copy (const char *source_root,
const char *dest_root,
GError **error);
/*
* Copyright © 2020 Collabora Ltd.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
* This program 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.1 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, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "subprojects/libglnx/config.h"
#include <locale.h>
#include <sysexits.h>
#include "libglnx/libglnx.h"
#include "glib-backports.h"
#include "utils.h"
static GOptionEntry options[] =
{
{ NULL }
};
int
main (int argc,
char *argv[])
{
g_autoptr(GOptionContext) context = NULL;
g_autoptr(GError) local_error = NULL;
GError **error = &local_error;
int ret = EX_USAGE;
setlocale (LC_ALL, "");
pv_avoid_gvfs ();
context = g_option_context_new ("SOURCE DEST");
g_option_context_add_main_entries (context, options, NULL);
if (!g_option_context_parse (context, &argc, &argv, error))
goto out;
if (argc >= 2 && strcmp (argv[1], "--") == 0)
{
argv++;
argc--;
}
if (argc != 3)
{
g_printerr ("Usage: %s SOURCE DEST\n", g_get_prgname ());
goto out;
}
ret = EX_UNAVAILABLE;
if (!pv_cheap_tree_copy (argv[1], argv[2], error))
goto out;
ret = 0;
out:
if (local_error != NULL)
g_warning ("%s", local_error->message);
return ret;
}
#!/usr/bin/env python3
# Copyright 2020 Collabora Ltd.
#
# SPDX-License-Identifier: MIT
import os
import subprocess
import tempfile
import unittest
try:
import typing
typing # placate pyflakes
except ImportError:
pass
class TestCheapCopy(unittest.TestCase):
def setUp(self) -> None:
self.G_TEST_SRCDIR = os.getenv(
'G_TEST_SRCDIR',
os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir),
),
)
self.G_TEST_BUILDDIR = os.getenv(
'G_TEST_BUILDDIR',
os.path.abspath('_build'),
)
self.cheap_copy = os.path.join(self.G_TEST_BUILDDIR, 'test-cheap-copy')
def assert_tree_is_superset(self, superset, subset):
for path, dirs, files in os.walk(subset):
equivalent = os.path.join(superset, os.path.relpath(path, subset))
for d in dirs:
if not os.path.isdir(os.path.join(equivalent, d)):
raise AssertionError(
'%r should be a directory', equivalent)
for f in files:
in_subset = os.path.join(path, f)
if (
os.path.islink(in_subset)
or not os.path.exists(in_subset)
):
target = os.readlink(in_subset)
target2 = os.readlink(os.path.join(equivalent, f))
self.assertEqual(target, target2)
else:
info = os.stat(in_subset)
info2 = os.stat(os.path.join(equivalent, f))
# they should be hard links
self.assertEqual(info.st_ino, info2.st_ino)
self.assertEqual(info.st_dev, info2.st_dev)
def assert_tree_is_same(self, left, right):
self.assert_tree_is_superset(left, right)
self.assert_tree_is_superset(right, left)
def test_empty(self) -> None:
with tempfile.TemporaryDirectory(
) as source, tempfile.TemporaryDirectory(
) as dest:
subprocess.run(
[
self.cheap_copy,
source,
dest,
],
check=True,
)
self.assert_tree_is_same(source, dest)
def test_create(self) -> None:
with tempfile.TemporaryDirectory(
) as source, tempfile.TemporaryDirectory(
) as parent:
dest = os.path.join(parent, 'dest')
subprocess.run(
[
self.cheap_copy,
source,
dest,
],
check=True,
)
self.assert_tree_is_same(source, dest)
def test_populated(self) -> None:
with tempfile.TemporaryDirectory(
) as source, tempfile.TemporaryDirectory(
) as parent:
os.makedirs(os.path.join(source, 'a', 'b', 'c'))
os.makedirs(os.path.join(source, 'files'))
with open(os.path.join(source, 'x'), 'w') as writer:
writer.write('hello')
with open(os.path.join(source, 'files', 'y'), 'w') as writer:
writer.write('hello')
os.symlink('y', os.path.join(source, 'files', 'exists'))
os.symlink('/dev', os.path.join(source, 'files', 'dev'))
os.symlink('no', os.path.join(source, 'files', 'not here'))
dest = os.path.join(parent, 'dest')
subprocess.run(
[
self.cheap_copy,
source,
dest,
],
check=True,
)
self.assert_tree_is_same(source, dest)
def tearDown(self) -> None:
pass
if __name__ == '__main__':
try:
from tap.runner import TAPTestRunner
except ImportError:
TAPTestRunner = None # type: ignore
if TAPTestRunner is not None:
runner = TAPTestRunner()
runner.set_stream(True)
unittest.main(testRunner=runner)
else:
print('1..1')
program = unittest.main(exit=False)
if program.result.wasSuccessful():
print(
'ok 1 - %r (tap module not available)'
% program.result
)
else:
print(
'not ok 1 - %r (tap module not available)'
% program.result
)
# vi: set sw=4 sts=4 et:
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment