text: /* 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 destination 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));
  
    /* Hotcopied FS is complete. Stamp it with a format file. */
    SVN_ERR(svn_fs_fs__write_format(dst_fs, TRUE, pool));
  
    return SVN_NO_ERROR;
  }
  
  svn_error_t *
  svn_fs_fs__hotcopy(svn_fs_t *src_fs,
                     svn_fs_t *dst_fs,
                     const char *src_path,
                     const char *dst_path,
                     svn_boolean_t incremental,
                     svn_fs_hotcopy_notify_t notify_func,
                     void *notify_baton,
                     svn_cancel_func_t cancel_func,
                     void *cancel_baton,
                     svn_mutex__t *common_pool_lock,
                     apr_pool_t *pool,
                     apr_pool_t *common_pool)
  {
    struct hotcopy_body_baton hbb;
  
    if (cancel_func)
      SVN_ERR(cancel_func(cancel_baton));
  
    SVN_ERR(svn_fs_fs__open(src_fs, src_path, 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)
          {
            /* No destination?  Fallback to a non-incremental hotcopy. */
            incremental = FALSE;
          }
      }
  
    if (incremental)
      {
        /* 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));
  
        SVN_ERR(svn_fs_fs__initialize_shared_data(dst_fs, common_pool_lock,
                                                  pool, common_pool));
        SVN_ERR(svn_fs_fs__initialize_caches(dst_fs, pool));
      }
    else
      {
        /* Start out with an empty destination using the same configuration
         * as the source. */
        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));
  
        SVN_ERR(svn_fs_fs__initialize_shared_data(dst_fs, common_pool_lock,
                                                  pool, common_pool));
        SVN_ERR(svn_fs_fs__initialize_caches(dst_fs, pool));
      }
  
    if (cancel_func)
      SVN_ERR(cancel_func(cancel_baton));
  
    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;
  
    /* Lock the destination in the incremental mode.  For a non-incremental
     * hotcopy, don't take any locks.  In that case the destination cannot be
     * opened until the hotcopy finishes, and we don't have to worry about
     * concurrency. */
    if (incremental)
      SVN_ERR(svn_fs_fs__with_all_locks(dst_fs, hotcopy_body, &hbb, pool));
    else
      SVN_ERR(hotcopy_body(&hbb, pool));
  
    return SVN_NO_ERROR;
  }
  