/* hotcopys.c --- FS hotcopy functionality for FSX * * ==================================================================== * 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_x.h" #include "hotcopy.h" #include "util.h" #include "revprops.h" #include "rep-cache.h" #include "transaction.h" #include "recovery.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 *result_pool) { svn_error_t *err = svn_path_cstring_to_utf8(name_p, name, result_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, result_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 *scratch_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(scratch_pool); /* The 'dst_path' is simply dst_parent/dst_basename */ dst_path = svn_dirent_join(dst_parent, dst_basename, scratch_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, scratch_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, scratch_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, scratch_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, scratch_pool)); status = apr_dir_close(this_dir); if (status) return svn_error_wrap_apr(status, _("Error closing directory '%s'"), svn_dirent_local_style(src, scratch_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; 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; svn_fs_x__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->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_x__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 SCRATCH_POOL for temporary allocations. */ static svn_error_t * hotcopy_remove_file(const char *path, apr_pool_t *scratch_pool) { /* Make the rev file writable and remove it. */ SVN_ERR(svn_io_set_file_read_write(path, TRUE, scratch_pool)); SVN_ERR(svn_io_remove_file2(path, TRUE, scratch_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) { svn_fs_x__data_t *src_ffd = src_fs->fsap_data; svn_fs_x__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 FSX format (%d) of the hotcopy source does not match the " "FSX 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 SCRATCH_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 *scratch_pool) { svn_error_t *err = svn_io_remove_dir2(path, TRUE, cancel_func, cancel_baton, scratch_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 SCRATCH_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 *scratch_pool) { svn_fs_x__data_t *src_ffd = src_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. */ SVN_ERR(svn_fs_x__read_min_unpacked_rev(&src_min_unpacked_rev, src_fs, scratch_pool)); SVN_ERR(svn_fs_x__read_min_unpacked_rev(&dst_min_unpacked_rev, dst_fs, scratch_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, scratch_pool)); if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); /* * Copy the necessary rev files. */ iterpool = svn_pool_create(scratch_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_x__write_current(dst_fs, pack_end_rev, 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)); 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_x__path_rev_shard(dst_fs, rev, iterpool), cancel_func, cancel_baton, iterpool)); if (rev > 0) SVN_ERR(remove_folder(svn_fs_x__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. 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_x__write_current(dst_fs, rev, 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; } /* Baton for hotcopy_body(). */ typedef struct hotcopy_body_baton_t { 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; } hotcopy_body_baton_t; /* 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 *scratch_pool) { hotcopy_body_baton_t *hbb = baton; svn_fs_t *src_fs = hbb->src_fs; svn_fs_t *dst_fs = hbb->dst_fs; 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; 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. */ SVN_ERR(svn_io_dir_file_copy(src_fs->path, dst_fs->path, PATH_CONFIG, scratch_pool)); 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_x__read_current(&src_youngest, src_fs, scratch_pool)); if (incremental) { SVN_ERR(svn_fs_x__youngest_rev(&dst_youngest, dst_fs, scratch_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, scratch_pool); dst_revs_dir = svn_dirent_join(dst_fs->path, PATH_REVS_DIR, scratch_pool); src_revprops_dir = svn_dirent_join(src_fs->path, PATH_REVPROPS_DIR, scratch_pool); dst_revprops_dir = svn_dirent_join(dst_fs->path, PATH_REVPROPS_DIR, scratch_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, scratch_pool)); SVN_ERR(svn_io_make_dir_recursively(dst_revprops_dir, scratch_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). */ 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, scratch_pool)); SVN_ERR(svn_fs_x__write_current(dst_fs, src_youngest, scratch_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, scratch_pool); SVN_ERR(svn_io_remove_dir2(dst_subdir, TRUE, cancel_func, cancel_baton, scratch_pool)); src_subdir = svn_dirent_join(src_fs->path, PATH_LOCKS_DIR, scratch_pool); SVN_ERR(svn_io_check_path(src_subdir, &kind, scratch_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, scratch_pool)); /* Now copy the node-origins cache tree. */ src_subdir = svn_dirent_join(src_fs->path, PATH_NODE_ORIGINS_DIR, scratch_pool); SVN_ERR(svn_io_check_path(src_subdir, &kind, scratch_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, scratch_pool)); /* * NB: Data copied below is only read by writers, not readers. * Writers are still locked out at this point. */ /* Copy the rep cache and then remove entries for revisions * younger than the destination's youngest revision. */ src_subdir = svn_dirent_join(src_fs->path, REP_CACHE_DB_NAME, scratch_pool); dst_subdir = svn_dirent_join(dst_fs->path, REP_CACHE_DB_NAME, scratch_pool); SVN_ERR(svn_io_check_path(src_subdir, &kind, scratch_pool)); if (kind == svn_node_file) { /* Copy the rep cache and then remove entries for revisions * that did not make it into the destination. */ SVN_ERR(svn_sqlite__hotcopy(src_subdir, dst_subdir, scratch_pool)); SVN_ERR(svn_fs_x__del_rep_reference(dst_fs, src_youngest, scratch_pool)); } /* Copy the txn-current file. */ SVN_ERR(svn_io_dir_file_copy(src_fs->path, dst_fs->path, PATH_TXN_CURRENT, scratch_pool)); /* If a revprop generation file exists in the source filesystem, * reset it to zero (since this is on a different path, it will not * overlap with data already in cache). Also, clean up stale files * used for the named atomics implementation. */ SVN_ERR(svn_fs_x__reset_revprop_generation_file(dst_fs, scratch_pool)); return SVN_NO_ERROR; } /* Wrapper around hotcopy_body taking out all necessary source repository * locks. */ static svn_error_t * hotcopy_locking_src_body(void *baton, apr_pool_t *scratch_pool) { hotcopy_body_baton_t *hbb = baton; return svn_error_trace(svn_fs_x__with_pack_lock(hbb->src_fs, hotcopy_body, baton, scratch_pool)); } /* 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 *scratch_pool) { svn_fs_x__data_t *src_ffd = src_fs->fsap_data; /* Create the DST_FS repository with the same layout as SRC_FS. */ SVN_ERR(svn_fs_x__create_file_tree(dst_fs, dst_path, src_ffd->format, src_ffd->max_files_per_dir, scratch_pool)); /* Copy the UUID. Hotcopy destination receives a new instance ID, but * has the same filesystem UUID as the source. */ SVN_ERR(svn_fs_x__set_uuid(dst_fs, src_fs->uuid, NULL, scratch_pool)); /* Remove revision 0 contents. Otherwise, it may not get overwritten * due to having a newer timestamp. */ SVN_ERR(hotcopy_remove_file(svn_fs_x__path_rev(dst_fs, 0, scratch_pool), scratch_pool)); SVN_ERR(hotcopy_remove_file(svn_fs_x__path_revprops(dst_fs, 0, scratch_pool), scratch_pool)); /* This filesystem is ready. Stamp it with a format number. Fail if * the 'format' file should already exist. */ SVN_ERR(svn_fs_x__write_format(dst_fs, FALSE, scratch_pool)); return SVN_NO_ERROR; } svn_error_t * svn_fs_x__hotcopy_prepare_target(svn_fs_t *src_fs, svn_fs_t *dst_fs, const char *dst_path, svn_boolean_t incremental, apr_pool_t *scratch_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, scratch_pool); SVN_ERR(svn_io_check_path(dst_format_abspath, &dst_format_kind, scratch_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, scratch_pool)); } else { /* Check the existing repository. */ SVN_ERR(svn_fs_x__open(dst_fs, dst_path, scratch_pool)); SVN_ERR(hotcopy_incremental_check_preconditions(src_fs, dst_fs)); } } 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, scratch_pool)); } return SVN_NO_ERROR; } svn_error_t * svn_fs_x__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 *scratch_pool) { hotcopy_body_baton_t 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_x__with_all_locks(dst_fs, hotcopy_locking_src_body, &hbb, scratch_pool)); return SVN_NO_ERROR; }