diff options
Diffstat (limited to 'subversion/libsvn_fs_fs/hotcopy.c')
-rw-r--r-- | subversion/libsvn_fs_fs/hotcopy.c | 1097 |
1 files changed, 1097 insertions, 0 deletions
diff --git a/subversion/libsvn_fs_fs/hotcopy.c b/subversion/libsvn_fs_fs/hotcopy.c new file mode 100644 index 0000000..43f513e --- /dev/null +++ b/subversion/libsvn_fs_fs/hotcopy.c @@ -0,0 +1,1097 @@ +/* hotcopy.c --- FS hotcopy functionality for FSFS + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ +#include "svn_pools.h" +#include "svn_path.h" +#include "svn_dirent_uri.h" + +#include "fs_fs.h" +#include "hotcopy.h" +#include "util.h" +#include "recovery.h" +#include "revprops.h" +#include "rep-cache.h" + +#include "../libsvn_fs/fs-loader.h" + +#include "svn_private_config.h" + +/* Like svn_io_dir_file_copy(), but doesn't copy files that exist at + * the destination and do not differ in terms of kind, size, and mtime. + * Set *SKIPPED_P to FALSE only if the file was copied, do not change + * the value in *SKIPPED_P otherwise. SKIPPED_P may be NULL if not + * required. */ +static svn_error_t * +hotcopy_io_dir_file_copy(svn_boolean_t *skipped_p, + const char *src_path, + const char *dst_path, + const char *file, + apr_pool_t *scratch_pool) +{ + const svn_io_dirent2_t *src_dirent; + const svn_io_dirent2_t *dst_dirent; + const char *src_target; + const char *dst_target; + + /* Does the destination already exist? If not, we must copy it. */ + dst_target = svn_dirent_join(dst_path, file, scratch_pool); + SVN_ERR(svn_io_stat_dirent2(&dst_dirent, dst_target, FALSE, TRUE, + scratch_pool, scratch_pool)); + if (dst_dirent->kind != svn_node_none) + { + /* If the destination's stat information indicates that the file + * is equal to the source, don't bother copying the file again. */ + src_target = svn_dirent_join(src_path, file, scratch_pool); + SVN_ERR(svn_io_stat_dirent2(&src_dirent, src_target, FALSE, FALSE, + scratch_pool, scratch_pool)); + if (src_dirent->kind == dst_dirent->kind && + src_dirent->special == dst_dirent->special && + src_dirent->filesize == dst_dirent->filesize && + src_dirent->mtime <= dst_dirent->mtime) + return SVN_NO_ERROR; + } + + if (skipped_p) + *skipped_p = FALSE; + + return svn_error_trace(svn_io_dir_file_copy(src_path, dst_path, file, + scratch_pool)); +} + +/* Set *NAME_P to the UTF-8 representation of directory entry NAME. + * NAME is in the internal encoding used by APR; PARENT is in + * UTF-8 and in internal (not local) style. + * + * Use PARENT only for generating an error string if the conversion + * fails because NAME could not be represented in UTF-8. In that + * case, return a two-level error in which the outer error's message + * mentions PARENT, but the inner error's message does not mention + * NAME (except possibly in hex) since NAME may not be printable. + * Such a compound error at least allows the user to go looking in the + * right directory for the problem. + * + * If there is any other error, just return that error directly. + * + * If there is any error, the effect on *NAME_P is undefined. + * + * *NAME_P and NAME may refer to the same storage. + */ +static svn_error_t * +entry_name_to_utf8(const char **name_p, + const char *name, + const char *parent, + apr_pool_t *pool) +{ + svn_error_t *err = svn_path_cstring_to_utf8(name_p, name, pool); + if (err && err->apr_err == APR_EINVAL) + { + return svn_error_createf(err->apr_err, err, + _("Error converting entry " + "in directory '%s' to UTF-8"), + svn_dirent_local_style(parent, pool)); + } + return err; +} + +/* Like svn_io_copy_dir_recursively() but doesn't copy regular files that + * exist in the destination and do not differ from the source in terms of + * kind, size, and mtime. Set *SKIPPED_P to FALSE only if at least one + * file was copied, do not change the value in *SKIPPED_P otherwise. + * SKIPPED_P may be NULL if not required. */ +static svn_error_t * +hotcopy_io_copy_dir_recursively(svn_boolean_t *skipped_p, + const char *src, + const char *dst_parent, + const char *dst_basename, + svn_boolean_t copy_perms, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_node_kind_t kind; + apr_status_t status; + const char *dst_path; + apr_dir_t *this_dir; + apr_finfo_t this_entry; + apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME; + + /* Make a subpool for recursion */ + apr_pool_t *subpool = svn_pool_create(pool); + + /* The 'dst_path' is simply dst_parent/dst_basename */ + dst_path = svn_dirent_join(dst_parent, dst_basename, pool); + + /* Sanity checks: SRC and DST_PARENT are directories, and + DST_BASENAME doesn't already exist in DST_PARENT. */ + SVN_ERR(svn_io_check_path(src, &kind, subpool)); + if (kind != svn_node_dir) + return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, + _("Source '%s' is not a directory"), + svn_dirent_local_style(src, pool)); + + SVN_ERR(svn_io_check_path(dst_parent, &kind, subpool)); + if (kind != svn_node_dir) + return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, + _("Destination '%s' is not a directory"), + svn_dirent_local_style(dst_parent, pool)); + + SVN_ERR(svn_io_check_path(dst_path, &kind, subpool)); + + /* Create the new directory. */ + /* ### TODO: copy permissions (needs apr_file_attrs_get()) */ + SVN_ERR(svn_io_make_dir_recursively(dst_path, pool)); + + /* Loop over the dirents in SRC. ('.' and '..' are auto-excluded) */ + SVN_ERR(svn_io_dir_open(&this_dir, src, subpool)); + + for (status = apr_dir_read(&this_entry, flags, this_dir); + status == APR_SUCCESS; + status = apr_dir_read(&this_entry, flags, this_dir)) + { + if ((this_entry.name[0] == '.') + && ((this_entry.name[1] == '\0') + || ((this_entry.name[1] == '.') + && (this_entry.name[2] == '\0')))) + { + continue; + } + else + { + const char *entryname_utf8; + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + SVN_ERR(entry_name_to_utf8(&entryname_utf8, this_entry.name, + src, subpool)); + if (this_entry.filetype == APR_REG) /* regular file */ + { + SVN_ERR(hotcopy_io_dir_file_copy(skipped_p, src, dst_path, + entryname_utf8, subpool)); + } + else if (this_entry.filetype == APR_LNK) /* symlink */ + { + const char *src_target = svn_dirent_join(src, entryname_utf8, + subpool); + const char *dst_target = svn_dirent_join(dst_path, + entryname_utf8, + subpool); + SVN_ERR(svn_io_copy_link(src_target, dst_target, + subpool)); + } + else if (this_entry.filetype == APR_DIR) /* recurse */ + { + const char *src_target; + + /* Prevent infinite recursion by filtering off our + newly created destination path. */ + if (strcmp(src, dst_parent) == 0 + && strcmp(entryname_utf8, dst_basename) == 0) + continue; + + src_target = svn_dirent_join(src, entryname_utf8, subpool); + SVN_ERR(hotcopy_io_copy_dir_recursively(skipped_p, + src_target, + dst_path, + entryname_utf8, + copy_perms, + cancel_func, + cancel_baton, + subpool)); + } + /* ### support other APR node types someday?? */ + + } + } + + if (! (APR_STATUS_IS_ENOENT(status))) + return svn_error_wrap_apr(status, _("Can't read directory '%s'"), + svn_dirent_local_style(src, pool)); + + status = apr_dir_close(this_dir); + if (status) + return svn_error_wrap_apr(status, _("Error closing directory '%s'"), + svn_dirent_local_style(src, pool)); + + /* Free any memory used by recursion */ + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + +/* Copy an un-packed revision or revprop file for revision REV from SRC_SUBDIR + * to DST_SUBDIR. Assume a sharding layout based on MAX_FILES_PER_DIR. + * Set *SKIPPED_P to FALSE only if the file was copied, do not change the + * value in *SKIPPED_P otherwise. SKIPPED_P may be NULL if not required. + * Use SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +hotcopy_copy_shard_file(svn_boolean_t *skipped_p, + const char *src_subdir, + const char *dst_subdir, + svn_revnum_t rev, + int max_files_per_dir, + apr_pool_t *scratch_pool) +{ + const char *src_subdir_shard = src_subdir, + *dst_subdir_shard = dst_subdir; + + if (max_files_per_dir) + { + const char *shard = apr_psprintf(scratch_pool, "%ld", + rev / max_files_per_dir); + src_subdir_shard = svn_dirent_join(src_subdir, shard, scratch_pool); + dst_subdir_shard = svn_dirent_join(dst_subdir, shard, scratch_pool); + + if (rev % max_files_per_dir == 0) + { + SVN_ERR(svn_io_make_dir_recursively(dst_subdir_shard, scratch_pool)); + SVN_ERR(svn_io_copy_perms(dst_subdir, dst_subdir_shard, + scratch_pool)); + } + } + + SVN_ERR(hotcopy_io_dir_file_copy(skipped_p, + src_subdir_shard, dst_subdir_shard, + apr_psprintf(scratch_pool, "%ld", rev), + scratch_pool)); + + return SVN_NO_ERROR; +} + + +/* Copy a packed shard containing revision REV, and which contains + * MAX_FILES_PER_DIR revisions, from SRC_FS to DST_FS. + * Update *DST_MIN_UNPACKED_REV in case the shard is new in DST_FS. + * Do not re-copy data which already exists in DST_FS. + * Set *SKIPPED_P to FALSE only if at least one part of the shard + * was copied, do not change the value in *SKIPPED_P otherwise. + * SKIPPED_P may be NULL if not required. + * Use SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +hotcopy_copy_packed_shard(svn_boolean_t *skipped_p, + svn_revnum_t *dst_min_unpacked_rev, + svn_fs_t *src_fs, + svn_fs_t *dst_fs, + svn_revnum_t rev, + int max_files_per_dir, + apr_pool_t *scratch_pool) +{ + const char *src_subdir; + const char *dst_subdir; + const char *packed_shard; + const char *src_subdir_packed_shard; + svn_revnum_t revprop_rev; + apr_pool_t *iterpool; + fs_fs_data_t *src_ffd = src_fs->fsap_data; + + /* Copy the packed shard. */ + src_subdir = svn_dirent_join(src_fs->path, PATH_REVS_DIR, scratch_pool); + dst_subdir = svn_dirent_join(dst_fs->path, PATH_REVS_DIR, scratch_pool); + packed_shard = apr_psprintf(scratch_pool, "%ld" PATH_EXT_PACKED_SHARD, + rev / max_files_per_dir); + src_subdir_packed_shard = svn_dirent_join(src_subdir, packed_shard, + scratch_pool); + SVN_ERR(hotcopy_io_copy_dir_recursively(skipped_p, src_subdir_packed_shard, + dst_subdir, packed_shard, + TRUE /* copy_perms */, + NULL /* cancel_func */, NULL, + scratch_pool)); + + /* Copy revprops belonging to revisions in this pack. */ + src_subdir = svn_dirent_join(src_fs->path, PATH_REVPROPS_DIR, scratch_pool); + dst_subdir = svn_dirent_join(dst_fs->path, PATH_REVPROPS_DIR, scratch_pool); + + if ( src_ffd->format < SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT + || src_ffd->min_unpacked_rev < rev + max_files_per_dir) + { + /* copy unpacked revprops rev by rev */ + iterpool = svn_pool_create(scratch_pool); + for (revprop_rev = rev; + revprop_rev < rev + max_files_per_dir; + revprop_rev++) + { + svn_pool_clear(iterpool); + + SVN_ERR(hotcopy_copy_shard_file(skipped_p, src_subdir, dst_subdir, + revprop_rev, max_files_per_dir, + iterpool)); + } + svn_pool_destroy(iterpool); + } + else + { + /* revprop for revision 0 will never be packed */ + if (rev == 0) + SVN_ERR(hotcopy_copy_shard_file(skipped_p, src_subdir, dst_subdir, + 0, max_files_per_dir, + scratch_pool)); + + /* packed revprops folder */ + packed_shard = apr_psprintf(scratch_pool, "%ld" PATH_EXT_PACKED_SHARD, + rev / max_files_per_dir); + src_subdir_packed_shard = svn_dirent_join(src_subdir, packed_shard, + scratch_pool); + SVN_ERR(hotcopy_io_copy_dir_recursively(skipped_p, + src_subdir_packed_shard, + dst_subdir, packed_shard, + TRUE /* copy_perms */, + NULL /* cancel_func */, NULL, + scratch_pool)); + } + + /* If necessary, update the min-unpacked rev file in the hotcopy. */ + if (*dst_min_unpacked_rev < rev + max_files_per_dir) + { + *dst_min_unpacked_rev = rev + max_files_per_dir; + SVN_ERR(svn_fs_fs__write_min_unpacked_rev(dst_fs, + *dst_min_unpacked_rev, + scratch_pool)); + } + + return SVN_NO_ERROR; +} + +/* Remove file PATH, if it exists - even if it is read-only. + * Use POOL for temporary allocations. */ +static svn_error_t * +hotcopy_remove_file(const char *path, + apr_pool_t *pool) +{ + /* Make the rev file writable and remove it. */ + SVN_ERR(svn_io_set_file_read_write(path, TRUE, pool)); + SVN_ERR(svn_io_remove_file2(path, TRUE, pool)); + + return SVN_NO_ERROR; +} + + +/* Remove revision or revprop files between START_REV (inclusive) and + * END_REV (non-inclusive) from folder DST_SUBDIR in DST_FS. Assume + * sharding as per MAX_FILES_PER_DIR. + * Use SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +hotcopy_remove_files(svn_fs_t *dst_fs, + const char *dst_subdir, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + int max_files_per_dir, + apr_pool_t *scratch_pool) +{ + const char *shard; + const char *dst_subdir_shard; + svn_revnum_t rev; + apr_pool_t *iterpool; + + /* Pre-compute paths for initial shard. */ + shard = apr_psprintf(scratch_pool, "%ld", start_rev / max_files_per_dir); + dst_subdir_shard = svn_dirent_join(dst_subdir, shard, scratch_pool); + + iterpool = svn_pool_create(scratch_pool); + for (rev = start_rev; rev < end_rev; rev++) + { + svn_pool_clear(iterpool); + + /* If necessary, update paths for shard. */ + if (rev != start_rev && rev % max_files_per_dir == 0) + { + shard = apr_psprintf(iterpool, "%ld", rev / max_files_per_dir); + dst_subdir_shard = svn_dirent_join(dst_subdir, shard, scratch_pool); + } + + /* remove files for REV */ + SVN_ERR(hotcopy_remove_file(svn_dirent_join(dst_subdir_shard, + apr_psprintf(iterpool, + "%ld", rev), + iterpool), + iterpool)); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Remove revisions between START_REV (inclusive) and END_REV (non-inclusive) + * from DST_FS. Assume sharding as per MAX_FILES_PER_DIR. + * Use SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +hotcopy_remove_rev_files(svn_fs_t *dst_fs, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + int max_files_per_dir, + apr_pool_t *scratch_pool) +{ + SVN_ERR_ASSERT(start_rev <= end_rev); + SVN_ERR(hotcopy_remove_files(dst_fs, + svn_dirent_join(dst_fs->path, + PATH_REVS_DIR, + scratch_pool), + start_rev, end_rev, + max_files_per_dir, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Remove revision properties between START_REV (inclusive) and END_REV + * (non-inclusive) from DST_FS. Assume sharding as per MAX_FILES_PER_DIR. + * Use SCRATCH_POOL for temporary allocations. Revision 0 revprops will + * not be deleted. */ +static svn_error_t * +hotcopy_remove_revprop_files(svn_fs_t *dst_fs, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + int max_files_per_dir, + apr_pool_t *scratch_pool) +{ + SVN_ERR_ASSERT(start_rev <= end_rev); + + /* don't delete rev 0 props */ + SVN_ERR(hotcopy_remove_files(dst_fs, + svn_dirent_join(dst_fs->path, + PATH_REVPROPS_DIR, + scratch_pool), + start_rev ? start_rev : 1, end_rev, + max_files_per_dir, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Verify that DST_FS is a suitable destination for an incremental + * hotcopy from SRC_FS. */ +static svn_error_t * +hotcopy_incremental_check_preconditions(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + apr_pool_t *pool) +{ + fs_fs_data_t *src_ffd = src_fs->fsap_data; + fs_fs_data_t *dst_ffd = dst_fs->fsap_data; + + /* We only support incremental hotcopy between the same format. */ + if (src_ffd->format != dst_ffd->format) + return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("The FSFS format (%d) of the hotcopy source does not match the " + "FSFS format (%d) of the hotcopy destination; please upgrade " + "both repositories to the same format"), + src_ffd->format, dst_ffd->format); + + /* Make sure the UUID of source and destination match up. + * We don't want to copy over a different repository. */ + if (strcmp(src_fs->uuid, dst_fs->uuid) != 0) + return svn_error_create(SVN_ERR_RA_UUID_MISMATCH, NULL, + _("The UUID of the hotcopy source does " + "not match the UUID of the hotcopy " + "destination")); + + /* Also require same shard size. */ + if (src_ffd->max_files_per_dir != dst_ffd->max_files_per_dir) + return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("The sharding layout configuration " + "of the hotcopy source does not match " + "the sharding layout configuration of " + "the hotcopy destination")); + return SVN_NO_ERROR; +} + +/* Remove folder PATH. Ignore errors due to the sub-tree not being empty. + * CANCEL_FUNC and CANCEL_BATON do the usual thing. + * Use POOL for temporary allocations. + */ +static svn_error_t * +remove_folder(const char *path, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_error_t *err = svn_io_remove_dir2(path, TRUE, + cancel_func, cancel_baton, pool); + + if (err && APR_STATUS_IS_ENOTEMPTY(err->apr_err)) + { + svn_error_clear(err); + err = SVN_NO_ERROR; + } + + return svn_error_trace(err); +} + +/* Copy the revision and revprop files (possibly sharded / packed) from + * SRC_FS to DST_FS. Do not re-copy data which already exists in DST_FS. + * When copying packed or unpacked shards, checkpoint the result in DST_FS + * for every shard by updating the 'current' file if necessary. Assume + * the >= SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT filesystem format without + * global next-ID counters. Indicate progress via the optional NOTIFY_FUNC + * callback using NOTIFY_BATON. Use POOL for temporary allocations. + */ +static svn_error_t * +hotcopy_revisions(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + svn_revnum_t src_youngest, + svn_revnum_t dst_youngest, + svn_boolean_t incremental, + const char *src_revs_dir, + const char *dst_revs_dir, + const char *src_revprops_dir, + const char *dst_revprops_dir, + svn_fs_hotcopy_notify_t notify_func, + void* notify_baton, + svn_cancel_func_t cancel_func, + void* cancel_baton, + apr_pool_t *pool) +{ + fs_fs_data_t *src_ffd = src_fs->fsap_data; + fs_fs_data_t *dst_ffd = dst_fs->fsap_data; + int max_files_per_dir = src_ffd->max_files_per_dir; + svn_revnum_t src_min_unpacked_rev; + svn_revnum_t dst_min_unpacked_rev; + svn_revnum_t rev; + apr_pool_t *iterpool; + + /* Copy the min unpacked rev, and read its value. */ + if (src_ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) + { + SVN_ERR(svn_fs_fs__read_min_unpacked_rev(&src_min_unpacked_rev, + src_fs, pool)); + SVN_ERR(svn_fs_fs__read_min_unpacked_rev(&dst_min_unpacked_rev, + dst_fs, pool)); + + /* We only support packs coming from the hotcopy source. + * The destination should not be packed independently from + * the source. This also catches the case where users accidentally + * swap the source and destination arguments. */ + if (src_min_unpacked_rev < dst_min_unpacked_rev) + return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("The hotcopy destination already contains " + "more packed revisions (%lu) than the " + "hotcopy source contains (%lu)"), + dst_min_unpacked_rev - 1, + src_min_unpacked_rev - 1); + + SVN_ERR(svn_io_dir_file_copy(src_fs->path, dst_fs->path, + PATH_MIN_UNPACKED_REV, pool)); + } + else + { + src_min_unpacked_rev = 0; + dst_min_unpacked_rev = 0; + } + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* + * Copy the necessary rev files. + */ + + iterpool = svn_pool_create(pool); + /* First, copy packed shards. */ + for (rev = 0; rev < src_min_unpacked_rev; rev += max_files_per_dir) + { + svn_boolean_t skipped = TRUE; + svn_revnum_t pack_end_rev; + + svn_pool_clear(iterpool); + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Copy the packed shard. */ + SVN_ERR(hotcopy_copy_packed_shard(&skipped, &dst_min_unpacked_rev, + src_fs, dst_fs, + rev, max_files_per_dir, + iterpool)); + + pack_end_rev = rev + max_files_per_dir - 1; + + /* Whenever this pack did not previously exist in the destination, + * update 'current' to the most recent packed rev (so readers can see + * new revisions which arrived in this pack). */ + if (pack_end_rev > dst_youngest) + { + SVN_ERR(svn_fs_fs__write_current(dst_fs, pack_end_rev, 0, 0, + iterpool)); + } + + /* When notifying about packed shards, make things simpler by either + * reporting a full revision range, i.e [pack start, pack end] or + * reporting nothing. There is one case when this approach might not + * be exact (incremental hotcopy with a pack replacing last unpacked + * revisions), but generally this is good enough. */ + if (notify_func && !skipped) + notify_func(notify_baton, rev, pack_end_rev, iterpool); + + /* Remove revision files which are now packed. */ + if (incremental) + { + SVN_ERR(hotcopy_remove_rev_files(dst_fs, rev, + rev + max_files_per_dir, + max_files_per_dir, iterpool)); + if (dst_ffd->format >= SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT) + SVN_ERR(hotcopy_remove_revprop_files(dst_fs, rev, + rev + max_files_per_dir, + max_files_per_dir, + iterpool)); + } + + /* Now that all revisions have moved into the pack, the original + * rev dir can be removed. */ + SVN_ERR(remove_folder(svn_fs_fs__path_rev_shard(dst_fs, rev, iterpool), + cancel_func, cancel_baton, iterpool)); + if (rev > 0 && dst_ffd->format >= SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT) + SVN_ERR(remove_folder(svn_fs_fs__path_revprops_shard(dst_fs, rev, + iterpool), + cancel_func, cancel_baton, iterpool)); + } + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + SVN_ERR_ASSERT(rev == src_min_unpacked_rev); + SVN_ERR_ASSERT(src_min_unpacked_rev == dst_min_unpacked_rev); + + /* Now, copy pairs of non-packed revisions and revprop files. + * If necessary, update 'current' after copying all files from a shard. */ + for (; rev <= src_youngest; rev++) + { + svn_boolean_t skipped = TRUE; + + svn_pool_clear(iterpool); + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Copying non-packed revisions is racy in case the source repository is + * being packed concurrently with this hotcopy operation. The race can + * happen with FS formats prior to SVN_FS_FS__MIN_PACK_LOCK_FORMAT that + * support packed revisions. With the pack lock, however, the race is + * impossible, because hotcopy and pack operations block each other. + * + * We assume that all revisions coming after 'min-unpacked-rev' really + * are unpacked and that's not necessarily true with concurrent packing. + * Don't try to be smart in this edge case, because handling it properly + * might require copying *everything* from the start. Just abort the + * hotcopy with an ENOENT (revision file moved to a pack, so it is no + * longer where we expect it to be). */ + + /* Copy the rev file. */ + SVN_ERR(hotcopy_copy_shard_file(&skipped, + src_revs_dir, dst_revs_dir, rev, + max_files_per_dir, + iterpool)); + /* Copy the revprop file. */ + SVN_ERR(hotcopy_copy_shard_file(&skipped, + src_revprops_dir, dst_revprops_dir, + rev, max_files_per_dir, + iterpool)); + + /* Whenever this revision did not previously exist in the destination, + * checkpoint the progress via 'current' (do that once per full shard + * in order not to slow things down). */ + if (rev > dst_youngest) + { + if (max_files_per_dir && (rev % max_files_per_dir == 0)) + { + SVN_ERR(svn_fs_fs__write_current(dst_fs, rev, 0, 0, + iterpool)); + } + } + + if (notify_func && !skipped) + notify_func(notify_baton, rev, rev, iterpool); + } + svn_pool_destroy(iterpool); + + /* We assume that all revisions were copied now, i.e. we didn't exit the + * above loop early. 'rev' was last incremented during exit of the loop. */ + SVN_ERR_ASSERT(rev == src_youngest + 1); + + return SVN_NO_ERROR; +} + +/* Shortcut for the revision and revprop copying for old (1 or 2) format + * filesystems without sharding and packing. Copy the non-sharded revision + * and revprop files from SRC_FS to DST_FS. Do not re-copy data which + * already exists in DST_FS. Do not somehow checkpoint the results in + * the 'current' file in DST_FS. Indicate progress via the optional + * NOTIFY_FUNC callback using NOTIFY_BATON. Use POOL for temporary + * allocations. Also see hotcopy_revisions(). + */ +static svn_error_t * +hotcopy_revisions_old(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + svn_revnum_t src_youngest, + const char *src_revs_dir, + const char *dst_revs_dir, + const char *src_revprops_dir, + const char *dst_revprops_dir, + svn_fs_hotcopy_notify_t notify_func, + void* notify_baton, + svn_cancel_func_t cancel_func, + void* cancel_baton, + apr_pool_t *pool) +{ + apr_pool_t *iterpool = svn_pool_create(pool); + svn_revnum_t rev; + + for (rev = 0; rev <= src_youngest; rev++) + { + svn_boolean_t skipped = TRUE; + + svn_pool_clear(iterpool); + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + SVN_ERR(hotcopy_io_dir_file_copy(&skipped, src_revs_dir, dst_revs_dir, + apr_psprintf(iterpool, "%ld", rev), + iterpool)); + SVN_ERR(hotcopy_io_dir_file_copy(&skipped, src_revprops_dir, + dst_revprops_dir, + apr_psprintf(iterpool, "%ld", rev), + iterpool)); + + if (notify_func && !skipped) + notify_func(notify_baton, rev, rev, iterpool); + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Baton for hotcopy_body(). */ +struct hotcopy_body_baton { + svn_fs_t *src_fs; + svn_fs_t *dst_fs; + svn_boolean_t incremental; + svn_fs_hotcopy_notify_t notify_func; + void *notify_baton; + svn_cancel_func_t cancel_func; + void *cancel_baton; +}; + +/* Perform a hotcopy, either normal or incremental. + * + * Normal hotcopy assumes that the destination exists as an empty + * directory. It behaves like an incremental hotcopy except that + * none of the copied files already exist in the destination. + * + * An incremental hotcopy copies only changed or new files to the destination, + * and removes files from the destination no longer present in the source. + * While the incremental hotcopy is running, readers should still be able + * to access the destintation repository without error and should not see + * revisions currently in progress of being copied. Readers are able to see + * new fully copied revisions even if the entire incremental hotcopy procedure + * has not yet completed. + * + * Writers are blocked out completely during the entire incremental hotcopy + * process to ensure consistency. This function assumes that the repository + * write-lock is held. + */ +static svn_error_t * +hotcopy_body(void *baton, apr_pool_t *pool) +{ + struct hotcopy_body_baton *hbb = baton; + svn_fs_t *src_fs = hbb->src_fs; + fs_fs_data_t *src_ffd = src_fs->fsap_data; + svn_fs_t *dst_fs = hbb->dst_fs; + fs_fs_data_t *dst_ffd = dst_fs->fsap_data; + svn_boolean_t incremental = hbb->incremental; + svn_fs_hotcopy_notify_t notify_func = hbb->notify_func; + void* notify_baton = hbb->notify_baton; + svn_cancel_func_t cancel_func = hbb->cancel_func; + void* cancel_baton = hbb->cancel_baton; + svn_revnum_t src_youngest; + apr_uint64_t src_next_node_id; + apr_uint64_t src_next_copy_id; + svn_revnum_t dst_youngest; + const char *src_revprops_dir; + const char *dst_revprops_dir; + const char *src_revs_dir; + const char *dst_revs_dir; + const char *src_subdir; + const char *dst_subdir; + svn_node_kind_t kind; + + /* Try to copy the config. + * + * ### We try copying the config file before doing anything else, + * ### because higher layers will abort the hotcopy if we throw + * ### an error from this function, and that renders the hotcopy + * ### unusable anyway. */ + if (src_ffd->format >= SVN_FS_FS__MIN_CONFIG_FILE) + { + svn_error_t *err; + + err = svn_io_dir_file_copy(src_fs->path, dst_fs->path, PATH_CONFIG, + pool); + if (err) + { + if (APR_STATUS_IS_ENOENT(err->apr_err)) + { + /* 1.6.0 to 1.6.11 did not copy the configuration file during + * hotcopy. So if we're hotcopying a repository which has been + * created as a hotcopy itself, it's possible that fsfs.conf + * does not exist. Ask the user to re-create it. + * + * ### It would be nice to make this a non-fatal error, + * ### but this function does not get an svn_fs_t object + * ### so we have no way of just printing a warning via + * ### the fs->warning() callback. */ + + const char *src_abspath; + const char *dst_abspath; + const char *config_relpath; + svn_error_t *err2; + + config_relpath = svn_dirent_join(src_fs->path, PATH_CONFIG, pool); + err2 = svn_dirent_get_absolute(&src_abspath, src_fs->path, pool); + if (err2) + return svn_error_trace(svn_error_compose_create(err, err2)); + err2 = svn_dirent_get_absolute(&dst_abspath, dst_fs->path, pool); + if (err2) + return svn_error_trace(svn_error_compose_create(err, err2)); + + /* ### hack: strip off the 'db/' directory from paths so + * ### they make sense to the user */ + src_abspath = svn_dirent_dirname(src_abspath, pool); + dst_abspath = svn_dirent_dirname(dst_abspath, pool); + + return svn_error_quick_wrapf(err, + _("Failed to create hotcopy at '%s'. " + "The file '%s' is missing from the source " + "repository. Please create this file, for " + "instance by running 'svnadmin upgrade %s'"), + dst_abspath, config_relpath, src_abspath); + } + else + return svn_error_trace(err); + } + } + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Find the youngest revision in the source and destination. + * We only support hotcopies from sources with an equal or greater amount + * of revisions than the destination. + * This also catches the case where users accidentally swap the + * source and destination arguments. */ + SVN_ERR(svn_fs_fs__read_current(&src_youngest, &src_next_node_id, + &src_next_copy_id, src_fs, pool)); + if (incremental) + { + SVN_ERR(svn_fs_fs__youngest_rev(&dst_youngest, dst_fs, pool)); + if (src_youngest < dst_youngest) + return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("The hotcopy destination already contains more revisions " + "(%lu) than the hotcopy source contains (%lu); are source " + "and destination swapped?"), + dst_youngest, src_youngest); + } + else + dst_youngest = 0; + + src_revs_dir = svn_dirent_join(src_fs->path, PATH_REVS_DIR, pool); + dst_revs_dir = svn_dirent_join(dst_fs->path, PATH_REVS_DIR, pool); + src_revprops_dir = svn_dirent_join(src_fs->path, PATH_REVPROPS_DIR, pool); + dst_revprops_dir = svn_dirent_join(dst_fs->path, PATH_REVPROPS_DIR, pool); + + /* Ensure that the required folders exist in the destination + * before actually copying the revisions and revprops. */ + SVN_ERR(svn_io_make_dir_recursively(dst_revs_dir, pool)); + SVN_ERR(svn_io_make_dir_recursively(dst_revprops_dir, pool)); + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Split the logic for new and old FS formats. The latter is much simpler + * due to the absense of sharding and packing. However, it requires special + * care when updating the 'current' file (which contains not just the + * revision number, but also the next-ID counters). */ + if (src_ffd->format >= SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT) + { + SVN_ERR(hotcopy_revisions(src_fs, dst_fs, src_youngest, dst_youngest, + incremental, src_revs_dir, dst_revs_dir, + src_revprops_dir, dst_revprops_dir, + notify_func, notify_baton, + cancel_func, cancel_baton, pool)); + SVN_ERR(svn_fs_fs__write_current(dst_fs, src_youngest, 0, 0, pool)); + } + else + { + SVN_ERR(hotcopy_revisions_old(src_fs, dst_fs, src_youngest, + src_revs_dir, dst_revs_dir, + src_revprops_dir, dst_revprops_dir, + notify_func, notify_baton, + cancel_func, cancel_baton, pool)); + SVN_ERR(svn_fs_fs__write_current(dst_fs, src_youngest, src_next_node_id, + src_next_copy_id, pool)); + } + + /* Replace the locks tree. + * This is racy in case readers are currently trying to list locks in + * the destination. However, we need to get rid of stale locks. + * This is the simplest way of doing this, so we accept this small race. */ + dst_subdir = svn_dirent_join(dst_fs->path, PATH_LOCKS_DIR, pool); + SVN_ERR(svn_io_remove_dir2(dst_subdir, TRUE, cancel_func, cancel_baton, + pool)); + src_subdir = svn_dirent_join(src_fs->path, PATH_LOCKS_DIR, pool); + SVN_ERR(svn_io_check_path(src_subdir, &kind, pool)); + if (kind == svn_node_dir) + SVN_ERR(svn_io_copy_dir_recursively(src_subdir, dst_fs->path, + PATH_LOCKS_DIR, TRUE, + cancel_func, cancel_baton, pool)); + + /* Now copy the node-origins cache tree. */ + src_subdir = svn_dirent_join(src_fs->path, PATH_NODE_ORIGINS_DIR, pool); + SVN_ERR(svn_io_check_path(src_subdir, &kind, pool)); + if (kind == svn_node_dir) + SVN_ERR(hotcopy_io_copy_dir_recursively(NULL, src_subdir, dst_fs->path, + PATH_NODE_ORIGINS_DIR, TRUE, + cancel_func, cancel_baton, pool)); + + /* + * NB: Data copied below is only read by writers, not readers. + * Writers are still locked out at this point. + */ + + if (dst_ffd->format >= SVN_FS_FS__MIN_REP_SHARING_FORMAT) + { + /* Copy the rep cache and then remove entries for revisions + * that did not make it into the destination. */ + src_subdir = svn_dirent_join(src_fs->path, REP_CACHE_DB_NAME, pool); + dst_subdir = svn_dirent_join(dst_fs->path, REP_CACHE_DB_NAME, pool); + SVN_ERR(svn_io_check_path(src_subdir, &kind, pool)); + if (kind == svn_node_file) + { + SVN_ERR(svn_sqlite__hotcopy(src_subdir, dst_subdir, pool)); + + /* The source might have r/o flags set on it - which would be + carried over to the copy. */ + SVN_ERR(svn_io_set_file_read_write(dst_subdir, FALSE, pool)); + SVN_ERR(svn_fs_fs__del_rep_reference(dst_fs, src_youngest, pool)); + } + } + + /* Copy the txn-current file. */ + if (dst_ffd->format >= SVN_FS_FS__MIN_TXN_CURRENT_FORMAT) + SVN_ERR(svn_io_dir_file_copy(src_fs->path, dst_fs->path, + PATH_TXN_CURRENT, pool)); + + return SVN_NO_ERROR; +} + +/* Create an empty filesystem at DST_FS at DST_PATH with the same + * configuration as SRC_FS (uuid, format, and other parameters). + * After creation DST_FS has no revisions, not even revision zero. */ +static svn_error_t * +hotcopy_create_empty_dest(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + const char *dst_path, + apr_pool_t *pool) +{ + fs_fs_data_t *src_ffd = src_fs->fsap_data; + + /* Create the DST_FS repository with the same layout as SRC_FS. */ + SVN_ERR(svn_fs_fs__create_file_tree(dst_fs, dst_path, src_ffd->format, + src_ffd->max_files_per_dir, + src_ffd->use_log_addressing, + pool)); + + /* Copy the UUID. Hotcopy destination receives a new instance ID, but + * has the same filesystem UUID as the source. */ + SVN_ERR(svn_fs_fs__set_uuid(dst_fs, src_fs->uuid, NULL, pool)); + + /* Remove revision 0 contents. Otherwise, it may not get overwritten + * due to having a newer timestamp. */ + SVN_ERR(hotcopy_remove_file(svn_fs_fs__path_rev(dst_fs, 0, pool), pool)); + SVN_ERR(hotcopy_remove_file(svn_fs_fs__path_revprops(dst_fs, 0, pool), + pool)); + + /* This filesystem is ready. Stamp it with a format number. Fail if + * the 'format' file should already exist. */ + SVN_ERR(svn_fs_fs__write_format(dst_fs, FALSE, pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_fs_fs__hotcopy_prepare_target(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + const char *dst_path, + svn_boolean_t incremental, + apr_pool_t *pool) +{ + if (incremental) + { + const char *dst_format_abspath; + svn_node_kind_t dst_format_kind; + + /* Check destination format to be sure we know how to incrementally + * hotcopy to the destination FS. */ + dst_format_abspath = svn_dirent_join(dst_path, PATH_FORMAT, pool); + SVN_ERR(svn_io_check_path(dst_format_abspath, &dst_format_kind, pool)); + if (dst_format_kind == svn_node_none) + { + /* Destination doesn't exist yet. Perform a normal hotcopy to a + * empty destination using the same configuration as the source. */ + SVN_ERR(hotcopy_create_empty_dest(src_fs, dst_fs, dst_path, pool)); + } + else + { + /* Check the existing repository. */ + SVN_ERR(svn_fs_fs__open(dst_fs, dst_path, pool)); + SVN_ERR(hotcopy_incremental_check_preconditions(src_fs, dst_fs, + pool)); + } + } + else + { + /* Start out with an empty destination using the same configuration + * as the source. */ + SVN_ERR(hotcopy_create_empty_dest(src_fs, dst_fs, dst_path, pool)); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_fs_fs__hotcopy(svn_fs_t *src_fs, + svn_fs_t *dst_fs, + svn_boolean_t incremental, + svn_fs_hotcopy_notify_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + struct hotcopy_body_baton hbb; + + hbb.src_fs = src_fs; + hbb.dst_fs = dst_fs; + hbb.incremental = incremental; + hbb.notify_func = notify_func; + hbb.notify_baton = notify_baton; + hbb.cancel_func = cancel_func; + hbb.cancel_baton = cancel_baton; + SVN_ERR(svn_fs_fs__with_all_locks(dst_fs, hotcopy_body, &hbb, pool)); + + return SVN_NO_ERROR; +} |