/* fs.c --- creating, opening and closing filesystems
 *
 * ====================================================================
 * Copyright (c) 2000-2006 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define APU_WANT_DB
#include <apu_want.h>

#include <apr_general.h>
#include <apr_pools.h>
#include <apr_file_io.h>

#include "svn_pools.h"
#include "svn_fs.h"
#include "svn_path.h"
#include "svn_utf.h"
#include "svn_delta.h"
#include "svn_version.h"
#include "fs.h"
#include "err.h"
#include "dag.h"
#include "revs-txns.h"
#include "uuid.h"
#include "tree.h"
#include "id.h"
#include "lock.h"
#include "svn_private_config.h"

#include "bdb/bdb-err.h"
#include "bdb/bdb_compat.h"
#include "bdb/env.h"
#include "bdb/nodes-table.h"
#include "bdb/rev-table.h"
#include "bdb/txn-table.h"
#include "bdb/copies-table.h"
#include "bdb/changes-table.h"
#include "bdb/reps-table.h"
#include "bdb/strings-table.h"
#include "bdb/uuids-table.h"
#include "bdb/locks-table.h"
#include "bdb/lock-tokens-table.h"

#include "../libsvn_fs/fs-loader.h"


/* Checking for return values, and reporting errors.  */

/* Check that we're using the right Berkeley DB version. */
/* FIXME: This check should be abstracted into the DB back-end layer. */
static svn_error_t *
check_bdb_version(void)
{
  int major, minor, patch;

  db_version(&major, &minor, &patch);

  /* First, check that we're using a reasonably correct of Berkeley DB. */
  if ((major < SVN_FS_WANT_DB_MAJOR)
      || (major == SVN_FS_WANT_DB_MAJOR && minor < SVN_FS_WANT_DB_MINOR)
      || (major == SVN_FS_WANT_DB_MAJOR && minor == SVN_FS_WANT_DB_MINOR
          && patch < SVN_FS_WANT_DB_PATCH))
    return svn_error_createf(SVN_ERR_FS_GENERAL, 0,
                             _("Bad database version: got %d.%d.%d,"
                               " should be at least %d.%d.%d"),
                             major, minor, patch,
                             SVN_FS_WANT_DB_MAJOR,
                             SVN_FS_WANT_DB_MINOR,
                             SVN_FS_WANT_DB_PATCH);

  /* Now, check that the version we're running against is the same as
     the one we compiled with. */
  if (major != DB_VERSION_MAJOR || minor != DB_VERSION_MINOR)
    return svn_error_createf(SVN_ERR_FS_GENERAL, 0,
                             _("Bad database version:"
                               " compiled with %d.%d.%d,"
                               " running against %d.%d.%d"),
                             DB_VERSION_MAJOR,
                             DB_VERSION_MINOR,
                             DB_VERSION_PATCH,
                             major, minor, patch);
  return SVN_NO_ERROR;
}


/* If FS is already open, then return an SVN_ERR_FS_ALREADY_OPEN
   error.  Otherwise, return zero.  */
static svn_error_t *
check_already_open(svn_fs_t *fs)
{
  if (fs->fsap_data)
    return svn_error_create(SVN_ERR_FS_ALREADY_OPEN, 0,
                            _("Filesystem object already open"));
  else
    return SVN_NO_ERROR;
}


/* Cleanup functions.  */

/* Close a database in the filesystem FS.
   DB_PTR is a pointer to the DB pointer in *FS to close.
   NAME is the name of the database, for use in error messages.  */
static svn_error_t *
cleanup_fs_db(svn_fs_t *fs, DB **db_ptr, const char *name)
{
  /* If the BDB environment is panicked, don't do anything, since
     attempting to close the database will fail anyway. */
  base_fs_data_t *bfd = fs->fsap_data;
  if (*db_ptr && !svn_fs_bdb__get_panic(bfd->bdb))
    {
      DB *db = *db_ptr;
      char *msg = apr_psprintf(fs->pool, "closing '%s' database", name);
      int db_err;

      *db_ptr = 0;
      db_err = db->close(db, 0);
      if (db_err == DB_RUNRECOVERY)
        {
          /* We can ignore DB_RUNRECOVERY errors from DB->close, but
             must set the panic flag in the environment baton.  The
             error will be propagated appropriately from
             svn_fs_bdb__close. */
          svn_fs_bdb__set_panic(bfd->bdb);
          db_err = 0;
        }

#if SVN_BDB_HAS_DB_INCOMPLETE
      /* We can ignore DB_INCOMPLETE on db->close and db->sync; it
       * just means someone else was using the db at the same time
       * we were.  See the Berkeley documentation at:
       * http://www.sleepycat.com/docs/ref/program/errorret.html#DB_INCOMPLETE
       * http://www.sleepycat.com/docs/api_c/db_close.html
       */
      if (db_err == DB_INCOMPLETE)
        db_err = 0;
#endif /* SVN_BDB_HAS_DB_INCOMPLETE */

      SVN_ERR(BDB_WRAP(fs, msg, db_err));
    }

  return SVN_NO_ERROR;
}

/* Close whatever Berkeley DB resources are allocated to FS.  */
static svn_error_t *
cleanup_fs(svn_fs_t *fs)
{
  base_fs_data_t *bfd = fs->fsap_data;
  bdb_env_baton_t *bdb = (bfd ? bfd->bdb : NULL);

  if (!bdb)
    return SVN_NO_ERROR;

  /* Close the databases.  */
  SVN_ERR(cleanup_fs_db(fs, &bfd->nodes, "nodes"));
  SVN_ERR(cleanup_fs_db(fs, &bfd->revisions, "revisions"));
  SVN_ERR(cleanup_fs_db(fs, &bfd->transactions, "transactions"));
  SVN_ERR(cleanup_fs_db(fs, &bfd->copies, "copies"));
  SVN_ERR(cleanup_fs_db(fs, &bfd->changes, "changes"));
  SVN_ERR(cleanup_fs_db(fs, &bfd->representations, "representations"));
  SVN_ERR(cleanup_fs_db(fs, &bfd->strings, "strings"));
  SVN_ERR(cleanup_fs_db(fs, &bfd->uuids, "uuids"));
  SVN_ERR(cleanup_fs_db(fs, &bfd->locks, "locks"));
  SVN_ERR(cleanup_fs_db(fs, &bfd->lock_tokens, "lock-tokens"));

  /* Finally, close the environment.  */
  bfd->bdb = 0;
  {
    svn_error_t *err = svn_fs_bdb__close(bdb);
    if (err)
      return svn_error_createf
        (err->apr_err, err,
         _("Berkeley DB error for filesystem '%s'"
           " while closing environment:\n"),
         fs->path);
  }
  return SVN_NO_ERROR;
}

#if 0   /* Set to 1 for instrumenting. */
static void print_fs_stats(svn_fs_t *fs)
{
  base_fs_data_t *bfd = fs->fsap_data;
  DB_TXN_STAT *t;
  DB_LOCK_STAT *l;
  int db_err;

  /* Print transaction statistics for this DB env. */
  if ((db_err = bfd->bdb->env->txn_stat(bfd->bdb->env, &t, 0)) != 0)
    fprintf(stderr, "Error running bfd->bdb->env->txn_stat(): %s",
            db_strerror(db_err));
  else
    {
      printf("*** DB transaction stats, right before closing env:\n");
      printf("   Number of transactions currently active: %d\n",
             t->st_nactive);
      printf("   Max number of active transactions at any one time: %d\n",
             t->st_maxnactive);
      printf("   Number of transactions that have begun: %d\n",
             t->st_nbegins);
      printf("   Number of transactions that have aborted: %d\n",
             t->st_naborts);
      printf("   Number of transactions that have committed: %d\n",
             t->st_ncommits);
      printf("   Number of times a thread was forced to wait: %d\n",
             t->st_region_wait);
      printf("   Number of times a thread didn't need to wait: %d\n",
             t->st_region_nowait);
      printf("*** End DB transaction stats.\n\n");
    }

  /* Print transaction statistics for this DB env. */
  if ((db_err = bfd->bdb->env->lock_stat(bfd->bdb->env, &l, 0)) != 0)
    fprintf(stderr, "Error running bfd->bdb->env->lock_stat(): %s",
            db_strerror(db_err));
  else
    {
      printf("*** DB lock stats, right before closing env:\n");
      printf("   The number of current locks: %d\n",
             l->st_nlocks);
      printf("   Max number of locks at any one time: %d\n",
             l->st_maxnlocks);
      printf("   Number of current lockers: %d\n",
             l->st_nlockers);
      printf("   Max number of lockers at any one time: %d\n",
             l->st_maxnlockers);
      printf("   Number of current objects: %d\n",
             l->st_nobjects);
      printf("   Max number of objects at any one time: %d\n",
             l->st_maxnobjects);
      printf("   Total number of locks requested: %d\n",
             l->st_nrequests);
      printf("   Total number of locks released: %d\n",
             l->st_nreleases);
      printf("   Total number of lock reqs failed because "
             "DB_LOCK_NOWAIT was set: %d\n", l->st_nnowaits);
      printf("   Total number of locks not immediately available "
             "due to conflicts: %d\n", l->st_nconflicts);
      printf("   Number of deadlocks detected: %d\n", l->st_ndeadlocks);
      printf("   Number of times a thread waited before "
             "obtaining the region lock: %d\n", l->st_region_wait);
      printf("   Number of times a thread didn't have to wait: %d\n",
             l->st_region_nowait);
      printf("*** End DB lock stats.\n\n");
    }

}
#else
#  define print_fs_stats(fs)
#endif /* 0/1 */

/* An APR pool cleanup function for a filesystem.  DATA must be a
   pointer to the filesystem to clean up.

   When the filesystem object's pool is freed, we want the resources
   held by Berkeley DB to go away, just like everything else.  So we
   register this cleanup function with the filesystem's pool, and let
   it take care of closing the databases, the environment, and any
   other DB objects we might be using.  APR calls this function before
   actually freeing the pool's memory.

   It's a pity that we can't return an svn_error_t object from an APR
   cleanup function.  For now, we return the rather generic
   SVN_ERR_FS_CLEANUP, and pass the real svn_error_t to the registered
   warning callback.  */

static apr_status_t
cleanup_fs_apr(void *data)
{
  svn_fs_t *fs = data;
  svn_error_t *err;

  print_fs_stats(fs);

  err = cleanup_fs(fs);
  if (! err)
    return APR_SUCCESS;

  /* Darn. An error during cleanup. Call the warning handler to
     try and do something "right" with this error. Note that
     the default will simply abort().  */
  (*fs->warning)(fs->warning_baton, err);

  svn_error_clear(err);

  return SVN_ERR_FS_CLEANUP;
}


static svn_error_t *
base_bdb_set_errcall(svn_fs_t *fs,
                     void (*db_errcall_fcn)(const char *errpfx, char *msg))
{
  base_fs_data_t *bfd = fs->fsap_data;

  SVN_ERR(svn_fs_base__check_fs(fs));
  bfd->bdb->error_info->user_callback = db_errcall_fcn;

  return SVN_NO_ERROR;
}


/* Write the DB_CONFIG file. */
static svn_error_t *
bdb_write_config  (svn_fs_t *fs)
{
  const char *dbconfig_file_name =
    svn_path_join(fs->path, BDB_CONFIG_FILE, fs->pool);
  apr_file_t *dbconfig_file = NULL;
  int i;

  static const char dbconfig_contents[] =
    "# This is the configuration file for the Berkeley DB environment\n"
    "# used by your Subversion repository.\n"
    "# You must run 'svnadmin recover' whenever you modify this file,\n"
    "# for your changes to take effect.\n"
    "\n"
    "### Lock subsystem\n"
    "#\n"
    "# Make sure you read the documentation at:\n"
    "#\n"
    "#   http://www.sleepycat.com/docs/ref/lock/max.html\n"
    "#\n"
    "# before tweaking these values.\n"
    "set_lk_max_locks   2000\n"
    "set_lk_max_lockers 2000\n"
    "set_lk_max_objects 2000\n"
    "\n"
    "### Log file subsystem\n"
    "#\n"
    "# Make sure you read the documentation at:\n"
    "#\n"
    "#   http://www.sleepycat.com/docs/api_c/env_set_lg_bsize.html\n"
    "#   http://www.sleepycat.com/docs/api_c/env_set_lg_max.html\n"
    "#   http://www.sleepycat.com/docs/ref/log/limits.html\n"
    "#\n"
    "# Increase the size of the in-memory log buffer from the default\n"
    "# of 32 Kbytes to 256 Kbytes.  Decrease the log file size from\n"
    "# 10 Mbytes to 1 Mbyte.  This will help reduce the amount of disk\n"
    "# space required for hot backups.  The size of the log file must be\n"
    "# at least four times the size of the in-memory log buffer.\n"
    "#\n"
    "# Note: Decreasing the in-memory buffer size below 256 Kbytes\n"
    "# will hurt commit performance. For details, see this post from\n"
    "# Daniel Berlin <dan@dberlin.org>:\n"
    "#\n"
    "# http://subversion.tigris.org/servlets/ReadMsg?list=dev&msgId=161960\n"
    "set_lg_bsize     262144\n"
    "set_lg_max      1048576\n"
    "#\n"
    "# If you see \"log region out of memory\" errors, bump lg_regionmax.\n"
    "# See http://www.sleepycat.com/docs/ref/log/config.html and\n"
    "# http://svn.haxx.se/users/archive-2004-10/1001.shtml for more.\n"
    "set_lg_regionmax 131072\n"
    "#\n"
    /* ### Configure this with "svnadmin create --bdb-cache-size" */
    "# The default cache size in BDB is only 256k. As explained in\n"
    "# http://svn.haxx.se/dev/archive-2004-12/0369.shtml, this is too\n"
    "# small for most applications. Bump this number if \"db_stat -m\"\n"
    "# shows too many cache misses.\n"
    "set_cachesize    0 1048576 1\n";

  /* Run-time configurable options.
     Each option set consists of a minimum required BDB version, a
     config hash key, a header, an inactive form and an active
     form. We always write the header; then, depending on the
     run-time configuration and the BDB version we're compiling
     against, we write either the active or inactive form of the
     value. */
  static const struct
  {
    int bdb_major;
    int bdb_minor;
    const char *config_key;
    const char *header;
    const char *inactive;
    const char *active;
  } dbconfig_options[] = {
    /* Controlled by "svnadmin create --bdb-txn-nosync" */
    { 4, 0, SVN_FS_CONFIG_BDB_TXN_NOSYNC,
      /* header */
      "#\n"
      "# Disable fsync of log files on transaction commit. Read the\n"
      "# documentation about DB_TXN_NOSYNC at:\n"
      "#\n"
      "#   http://www.sleepycat.com/docs/api_c/env_set_flags.html\n"
      "#\n"
      "# [requires Berkeley DB 4.0]\n",
      /* inactive */
      "# set_flags DB_TXN_NOSYNC\n",
      /* active */
      "set_flags DB_TXN_NOSYNC\n" },
    /* Controlled by "svnadmin create --bdb-log-keep" */
    { 4, 2, SVN_FS_CONFIG_BDB_LOG_AUTOREMOVE,
      /* header */
      "#\n"
      "# Enable automatic removal of unused transaction log files.\n"
      "# Read the documentation about DB_LOG_AUTOREMOVE at:\n"
      "#\n"
      "#   http://www.sleepycat.com/docs/api_c/env_set_flags.html\n"
      "#\n"
      "# [requires Berkeley DB 4.2]\n",
      /* inactive */
      "# set_flags DB_LOG_AUTOREMOVE\n",
      /* active */
      "set_flags DB_LOG_AUTOREMOVE\n" },
  };
  static const int dbconfig_options_length =
    sizeof(dbconfig_options)/sizeof(*dbconfig_options);


  SVN_ERR(svn_io_file_open(&dbconfig_file, dbconfig_file_name,
                           APR_WRITE | APR_CREATE, APR_OS_DEFAULT,
                           fs->pool));

  SVN_ERR(svn_io_file_write_full(dbconfig_file, dbconfig_contents,
                                 sizeof(dbconfig_contents) - 1, NULL,
                                 fs->pool));

  /* Write the variable DB_CONFIG flags. */
  for (i = 0; i < dbconfig_options_length; ++i)
    {
      void *value = NULL;
      const char *choice;

      if (fs->config)
        {
          value = apr_hash_get(fs->config,
                               dbconfig_options[i].config_key,
                               APR_HASH_KEY_STRING);
        }

      SVN_ERR(svn_io_file_write_full(dbconfig_file,
                                     dbconfig_options[i].header,
                                     strlen(dbconfig_options[i].header),
                                     NULL, fs->pool));

      if (((DB_VERSION_MAJOR == dbconfig_options[i].bdb_major
            && DB_VERSION_MINOR >= dbconfig_options[i].bdb_minor)
           || DB_VERSION_MAJOR > dbconfig_options[i].bdb_major)
          && value != NULL && strcmp(value, "0") != 0)
        choice = dbconfig_options[i].active;
      else
        choice = dbconfig_options[i].inactive;

      SVN_ERR(svn_io_file_write_full(dbconfig_file, choice, strlen(choice),
                                     NULL, fs->pool));
    }

  SVN_ERR(svn_io_file_close(dbconfig_file, fs->pool));

  return SVN_NO_ERROR;
}



static svn_error_t *
base_serialized_init(svn_fs_t *fs, apr_pool_t *common_pool, apr_pool_t *pool)
{
  /* Nothing to do here. */
  return SVN_NO_ERROR;
}



/* Creating a new filesystem */

static fs_vtable_t fs_vtable = {
  base_serialized_init,
  svn_fs_base__youngest_rev,
  svn_fs_base__revision_prop,
  svn_fs_base__revision_proplist,
  svn_fs_base__change_rev_prop,
  svn_fs_base__get_uuid,
  svn_fs_base__set_uuid,
  svn_fs_base__revision_root,
  svn_fs_base__begin_txn,
  svn_fs_base__open_txn,
  svn_fs_base__purge_txn,
  svn_fs_base__list_transactions,
  svn_fs_base__deltify,
  svn_fs_base__lock,
  svn_fs_base__generate_lock_token,
  svn_fs_base__unlock,
  svn_fs_base__get_lock,
  svn_fs_base__get_locks,
  base_bdb_set_errcall,
};

/* Where the format number is stored. */
#define FORMAT_FILE   "format"

/* Depending on CREATE, create or open the environment and databases
   for filesystem FS in PATH. Use POOL for temporary allocations. */
static svn_error_t *
open_databases(svn_fs_t *fs, svn_boolean_t create,
               const char *path, apr_pool_t *pool)
{
  base_fs_data_t *bfd;

  SVN_ERR(check_already_open(fs));

  bfd = apr_pcalloc(fs->pool, sizeof(*bfd));
  fs->vtable = &fs_vtable;
  fs->fsap_data = bfd;

  /* Initialize the fs's path. */
  fs->path = apr_pstrdup(fs->pool, path);

  if (create)
    SVN_ERR(bdb_write_config(fs));

  /* Create the Berkeley DB environment.  */
  {
    svn_error_t *err = svn_fs_bdb__open(&(bfd->bdb), path,
                                        SVN_BDB_STANDARD_ENV_FLAGS,
                                        0666, fs->pool);
    if (err)
      {
        if (create)
          return svn_error_createf
            (err->apr_err, err,
             _("Berkeley DB error for filesystem '%s'"
               " while creating environment:\n"),
             fs->path);
        else
          return svn_error_createf
            (err->apr_err, err,
             _("Berkeley DB error for filesystem '%s'"
               " while opening environment:\n"),
             fs->path);
      }
  }

  /* We must register the FS cleanup function *after* opening the
     environment, so that it's run before the environment baton
     cleanup. */
  apr_pool_cleanup_register(fs->pool, fs, cleanup_fs_apr,
                            apr_pool_cleanup_null);


  /* Create the databases in the environment.  */
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'nodes' table"
                        : "opening 'nodes' table"),
                   svn_fs_bdb__open_nodes_table(&bfd->nodes,
                                                bfd->bdb->env,
                                                create)));
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'revisions' table"
                        : "opening 'revisions' table"),
                   svn_fs_bdb__open_revisions_table(&bfd->revisions,
                                                    bfd->bdb->env,
                                                    create)));
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'transactions' table"
                        : "opening 'transactions' table"),
                   svn_fs_bdb__open_transactions_table(&bfd->transactions,
                                                       bfd->bdb->env,
                                                       create)));
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'copies' table"
                        : "opening 'copies' table"),
                   svn_fs_bdb__open_copies_table(&bfd->copies,
                                                 bfd->bdb->env,
                                                 create)));
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'changes' table"
                        : "opening 'changes' table"),
                   svn_fs_bdb__open_changes_table(&bfd->changes,
                                                  bfd->bdb->env,
                                                  create)));
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'representations' table"
                        : "opening 'representations' table"),
                   svn_fs_bdb__open_reps_table(&bfd->representations,
                                               bfd->bdb->env,
                                               create)));
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'strings' table"
                        : "opening 'strings' table"),
                   svn_fs_bdb__open_strings_table(&bfd->strings,
                                                  bfd->bdb->env,
                                                  create)));
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'uuids' table"
                        : "opening 'uuids' table"),
                   svn_fs_bdb__open_uuids_table(&bfd->uuids,
                                                bfd->bdb->env,
                                                create)));
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'locks' table"
                        : "opening 'locks' table"),
                   svn_fs_bdb__open_locks_table(&bfd->locks,
                                                bfd->bdb->env,
                                                create)));
  SVN_ERR(BDB_WRAP(fs, (create
                        ? "creating 'lock-nodes' table"
                        : "opening 'lock-nodes' table"),
                   svn_fs_bdb__open_lock_tokens_table(&bfd->lock_tokens,
                                                      bfd->bdb->env,
                                                      create)));

  return SVN_NO_ERROR;
}


static svn_error_t *
base_create(svn_fs_t *fs, const char *path, apr_pool_t *pool)
{
  int format = SVN_FS_BASE__FORMAT_NUMBER;
  
  /* Create the environment and databases. */
  svn_error_t *svn_err = open_databases(fs, TRUE, path, pool);
  if (svn_err) goto error;

  /* Initialize the DAG subsystem. */
  svn_err = svn_fs_base__dag_init_fs(fs);
  if (svn_err) goto error;

  /* See if we had an explicitly specified pre 1.4 compatible.  */
  if (fs->config && apr_hash_get(fs->config, SVN_FS_CONFIG_PRE_1_4_COMPATIBLE,
                                 APR_HASH_KEY_STRING))
    format = 1;

  /* This filesystem is ready.  Stamp it with a format number. */
  svn_err = svn_io_write_version_file
    (svn_path_join(fs->path, FORMAT_FILE, pool), format, pool);
  if (svn_err) goto error;

  return SVN_NO_ERROR;

error:
  svn_error_clear(cleanup_fs(fs));
  return svn_err;
}


/* Gaining access to an existing Berkeley DB-based filesystem.  */


/* Return the error SVN_ERR_FS_UNSUPPORTED_FORMAT if FS's format
   number is not the same as the format number supported by this
   Subversion. */
static svn_error_t *
check_format(int format)
{
  /* We support format 1 and 2 simultaneously.  */
  if (format == 1 && SVN_FS_BASE__FORMAT_NUMBER == 2)
    return SVN_NO_ERROR;

  if (format != SVN_FS_BASE__FORMAT_NUMBER)
    {
      return svn_error_createf 
        (SVN_ERR_FS_UNSUPPORTED_FORMAT, NULL,
         _("Expected FS format '%d'; found format '%d'"), 
         SVN_FS_BASE__FORMAT_NUMBER, format);
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
base_open(svn_fs_t *fs, const char *path, apr_pool_t *pool)
{
  int format;

  /* Create the environment and databases. */
  svn_error_t *svn_err = open_databases(fs, FALSE, path, pool);
  if (svn_err) goto error;

  /* Read the FS format number. */
  svn_err = svn_io_read_version_file
    (&format, svn_path_join(fs->path, FORMAT_FILE, pool), pool);
  if (svn_err && APR_STATUS_IS_ENOENT(svn_err->apr_err))
    {
      /* Pre-1.2 filesystems did not have a format file (you could say
         they were format "0"), so they get upgraded on the fly. */
      svn_error_clear(svn_err);
      format = SVN_FS_BASE__FORMAT_NUMBER;
      svn_err = svn_io_write_version_file
        (svn_path_join(fs->path, FORMAT_FILE, pool), format, pool);
      if (svn_err) goto error;
    }
  else if (svn_err)
    goto error;

  /* Now we've got a format number no matter what. */
  ((base_fs_data_t *) fs->fsap_data)->format = format;
  SVN_ERR(check_format(format));

  return SVN_NO_ERROR;

 error:
  svn_error_clear(cleanup_fs(fs));
  return svn_err;
}


/* Running recovery on a Berkeley DB-based filesystem.  */


/* Recover a database at PATH. Perform catastrophic recovery if FATAL
   is TRUE. Use POOL for temporary allocation. */
static svn_error_t *
bdb_recover(const char *path, svn_boolean_t fatal, apr_pool_t *pool)
{
  bdb_env_baton_t *bdb;

  /* Here's the comment copied from db_recover.c:

     Initialize the environment -- we don't actually do anything
     else, that all that's needed to run recovery.

     Note that we specify a private environment, as we're about to
     create a region, and we don't want to leave it around.  If we
     leave the region around, the application that should create it
     will simply join it instead, and will then be running with
     incorrectly sized (and probably terribly small) caches.  */

  /* Note that since we're using a private environment, we shoudl
     /not/ initialize locking. We want the environment files to go
     away. */

  SVN_ERR(svn_fs_bdb__open(&bdb, path,
                           ((fatal ? DB_RECOVER_FATAL : DB_RECOVER)
                            | SVN_BDB_PRIVATE_ENV_FLAGS),
                           0666, pool));
  SVN_ERR(svn_fs_bdb__close(bdb));

  return SVN_NO_ERROR;
}

static svn_error_t *
base_bdb_recover(const char *path,
                 apr_pool_t *pool)
{
  return bdb_recover(path, FALSE, pool);
}



/* Running the 'archive' command on a Berkeley DB-based filesystem.  */


static svn_error_t *
base_bdb_logfiles(apr_array_header_t **logfiles,
                  const char *path,
                  svn_boolean_t only_unused,
                  apr_pool_t *pool)
{
  bdb_env_baton_t *bdb;
  char **filelist;
  char **filename;
  u_int32_t flags = only_unused ? 0 : DB_ARCH_LOG;

  *logfiles = apr_array_make(pool, 4, sizeof(const char *));

  SVN_ERR(svn_fs_bdb__open(&bdb, path,
                           SVN_BDB_STANDARD_ENV_FLAGS,
                           0666, pool));
  SVN_BDB_ERR(bdb, bdb->env->log_archive(bdb->env, &filelist, flags));

  if (filelist == NULL)
    return svn_fs_bdb__close(bdb);

  for (filename = filelist; *filename != NULL; ++filename)
    {
      APR_ARRAY_PUSH(*logfiles, const char *) = apr_pstrdup(pool, *filename);
    }

  free(filelist);

  return svn_fs_bdb__close(bdb);
}



/* Copying a live Berkeley DB-base filesystem.  */

/**
 * Delete all unused log files from DBD enviroment at @a live_path that exist
 * in @a backup_path.
 */
static svn_error_t *
svn_fs_base__clean_logs(const char *live_path,
                        const char *backup_path,
                        apr_pool_t *pool)
{
  apr_array_header_t *logfiles;

  SVN_ERR(base_bdb_logfiles(&logfiles,
                            live_path,
                            TRUE,        /* Only unused logs */
                            pool));

  {  /* Process unused logs from live area */
    int idx;
    apr_pool_t *sub_pool = svn_pool_create(pool);

    /* Process log files. */
    for (idx = 0; idx < logfiles->nelts; idx++)
      {
        const char *log_file = APR_ARRAY_IDX(logfiles, idx, const char *);
        const char *live_log_path;
        const char *backup_log_path;

        svn_pool_clear(sub_pool);
        live_log_path = svn_path_join(live_path, log_file, sub_pool);
        backup_log_path = svn_path_join(backup_path, log_file, sub_pool);

        { /* Compare files. No point in using MD5 and wasting CPU cycles as we
             got full copies of both logs */

          svn_boolean_t files_match = FALSE;
          svn_node_kind_t kind;

          /* Check to see if there is a corresponding log file in the backup
             directory */
          SVN_ERR(svn_io_check_path(backup_log_path, &kind, pool));

          /* If the copy of the log exists, compare them */
          if (kind == svn_node_file)
            SVN_ERR(svn_io_files_contents_same_p(&files_match,
                                                 live_log_path,
                                                 backup_log_path,
                                                 sub_pool));

          /* If log files do not match, go to the next log filr. */
          if (files_match == FALSE)
            continue;
        }

        SVN_ERR(svn_io_remove_file(live_log_path, sub_pool));
      }

    svn_pool_destroy(sub_pool);
  }

  return SVN_NO_ERROR;
}


/* ### There -must- be a more elegant way to do a compile-time check
       for BDB 4.2 or later.  We're doing this because apparently
       env->get_flags() and DB->get_pagesize() don't exist in earlier
       versions of BDB.  */
#ifdef DB_LOG_AUTOREMOVE

/* Open the BDB environment at PATH and compare its configuration
   flags with FLAGS.  If every flag in FLAGS is set in the
   environment, then set *MATCH to true.  Else set *MATCH to false. */
static svn_error_t *
check_env_flags(svn_boolean_t *match,
                u_int32_t flags,
                const char *path,
                apr_pool_t *pool)
{
  bdb_env_baton_t *bdb;
  u_int32_t envflags;

  SVN_ERR(svn_fs_bdb__open(&bdb, path,
                           SVN_BDB_STANDARD_ENV_FLAGS,
                           0666, pool));

  SVN_BDB_ERR(bdb, bdb->env->get_flags(bdb->env, &envflags));
  SVN_ERR(svn_fs_bdb__close(bdb));

  if (flags & envflags)
    *match = TRUE;
  else
    *match = FALSE;

  return SVN_NO_ERROR;
}


/* Set *PAGESIZE to the size of pages used to hold items in the
   database environment located at PATH.
*/
static svn_error_t *
get_db_pagesize(u_int32_t *pagesize,
                const char *path,
                apr_pool_t *pool)
{
  bdb_env_baton_t *bdb;
  DB *nodes_table;

  SVN_ERR(svn_fs_bdb__open(&bdb, path,
                           SVN_BDB_STANDARD_ENV_FLAGS,
                           0666, pool));

  /* ### We're only asking for the pagesize on the 'nodes' table.
         Is this enough?  We never call DB->set_pagesize() on any of
         our tables, so presumably BDB is using the same default
         pagesize for all our databases, right? */
  SVN_BDB_ERR(bdb, svn_fs_bdb__open_nodes_table(&nodes_table, bdb->env,
                                                FALSE));
  SVN_BDB_ERR(bdb, nodes_table->get_pagesize(nodes_table, pagesize));
  SVN_BDB_ERR(bdb, nodes_table->close(nodes_table, 0));

  return svn_fs_bdb__close(bdb);
}
#endif /* DB_LOG_AUTOREMOVE */


/* Copy FILENAME from SRC_DIR to DST_DIR in byte increments of size
   CHUNKSIZE.  The read/write buffer of size CHUNKSIZE will be
   allocated in POOL. */
static svn_error_t *
copy_db_file_safely(const char *src_dir,
                    const char *dst_dir,
                    const char *filename,
                    u_int32_t chunksize,
                    apr_pool_t *pool)
{
  apr_file_t *s = NULL, *d = NULL;  /* init to null important for APR */
  const char *file_src_path = svn_path_join(src_dir, filename, pool);
  const char *file_dst_path = svn_path_join(dst_dir, filename, pool);  
  char *buf;

  /* Open source file. */
  SVN_ERR(svn_io_file_open(&s, file_src_path,
                           (APR_READ | APR_LARGEFILE | APR_BINARY),
                           APR_OS_DEFAULT, pool));

  /* Open destination file. */
  SVN_ERR(svn_io_file_open(&d, file_dst_path, (APR_WRITE | APR_CREATE |
                                               APR_LARGEFILE | APR_BINARY),
                           APR_OS_DEFAULT, pool));

  /* Allocate our read/write buffer. */
  buf = apr_palloc(pool, chunksize);

  /* Copy bytes till the cows come home. */
  while (1) 
    {
      apr_size_t bytes_this_time = chunksize;
      svn_error_t *read_err, *write_err;
      
      /* Read 'em. */
      if ((read_err = svn_io_file_read(s, buf, &bytes_this_time, pool)))
        {
          if (APR_STATUS_IS_EOF(read_err->apr_err))
            svn_error_clear(read_err);
          else
            {
              svn_error_clear(svn_io_file_close(s, pool));
              svn_error_clear(svn_io_file_close(d, pool));
              return read_err;
            }
        }
    
      /* Write 'em. */
      if ((write_err = svn_io_file_write_full(d, buf, bytes_this_time, NULL,
                                              pool)))
        {
          svn_error_clear(svn_io_file_close(s, pool));
          svn_error_clear(svn_io_file_close(d, pool));
          return write_err;
        }

      /* read_err is either NULL, or a dangling pointer - but it is only a
         dangling pointer if it used to be an EOF error. */
      if (read_err)
        {
          SVN_ERR(svn_io_file_close(s, pool));
          SVN_ERR(svn_io_file_close(d, pool));
          break;  /* got EOF on read, all files closed, all done. */
        }
    }

  return SVN_NO_ERROR;
}




static svn_error_t *
base_hotcopy(const char *src_path,
             const char *dest_path,
             svn_boolean_t clean_logs,
             apr_pool_t *pool)
{
  svn_error_t *err;
  u_int32_t pagesize;
  svn_boolean_t log_autoremove = FALSE;
  int format;

  /* Check the FS format number to be certain that we know how to
     hotcopy this FS.  Pre-1.2 filesystems did not have a format file (you
     could say they were format "0"), so we will error here.  This is not
     optimal, but since this has been the case since 1.2.0, and no one has
     complained, it apparently isn't much of a concern.  (We did not check
     the 'format' file in 1.2.x, but we did blindly try to copy 'locks',
     which would have errored just the same.)  */
  SVN_ERR(svn_io_read_version_file
          (&format, svn_path_join(src_path, FORMAT_FILE, pool), pool));
  SVN_ERR(check_format(format));

  /* If using DB 4.2 or later, note whether the DB_LOG_AUTOREMOVE
     feature is on.  If it is, we have a potential race condition:
     another process might delete a logfile while we're in the middle
     of copying all the logfiles.  (This is not a huge deal; at worst,
     the hotcopy fails with a file-not-found error.) */
#ifdef DB_LOG_AUTOREMOVE
  SVN_ERR(check_env_flags(&log_autoremove, DB_LOG_AUTOREMOVE,
                          src_path, pool));
#endif

  /* Copy the DB_CONFIG file. */
  SVN_ERR(svn_io_dir_file_copy(src_path, dest_path, "DB_CONFIG", pool));

  /* In order to copy the database files safely and atomically, we
     must copy them in chunks which are multiples of the page-size
     used by BDB.  See sleepycat docs for details, or svn issue #1818. */
#ifdef DB_LOG_AUTOREMOVE
  SVN_ERR(get_db_pagesize(&pagesize, src_path, pool));
  if (pagesize < SVN__STREAM_CHUNK_SIZE)
    {
      /* use the largest multiple of BDB pagesize we can. */
      int multiple = SVN__STREAM_CHUNK_SIZE / pagesize;
      pagesize *= multiple;
    }
#else
  /* default to 128K chunks, which should be safe.
     BDB almost certainly uses a power-of-2 pagesize. */
  pagesize = (4096 * 32); 
#endif

  /* Copy the databases.  */
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "nodes", pagesize, pool));
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "transactions", pagesize, pool));
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "revisions", pagesize, pool));
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "copies", pagesize, pool));
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "changes", pagesize, pool));
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "representations", pagesize, pool));
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "strings", pagesize, pool));
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "uuids", pagesize, pool));
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "locks", pagesize, pool));
  SVN_ERR(copy_db_file_safely(src_path, dest_path,
                              "lock-tokens", pagesize, pool));

  {
    apr_array_header_t *logfiles;
    int idx;
    apr_pool_t *subpool;

    SVN_ERR(base_bdb_logfiles(&logfiles,
                              src_path,
                              FALSE,   /* All logs */
                              pool));

    /* Process log files. */
    subpool = svn_pool_create(pool);
    for (idx = 0; idx < logfiles->nelts; idx++)
      {
        svn_pool_clear(subpool);
        err = svn_io_dir_file_copy(src_path, dest_path,
                                   APR_ARRAY_IDX(logfiles, idx,
                                                 const char *),
                                   subpool);
        if (err)
          {
            if (log_autoremove)
              return
                svn_error_quick_wrap 
                (err,
                 _("Error copying logfile;  the DB_LOG_AUTOREMOVE feature \n"
                   "may be interfering with the hotcopy algorithm.  If \n"
                   "the problem persists, try deactivating this feature \n"
                   "in DB_CONFIG"));
            else
              return err;
          }
      }
    svn_pool_destroy(subpool);
  }

  /* Since this is a copy we will have exclusive access to the repository. */
  err = bdb_recover(dest_path, TRUE, pool);
  if (err)
    {
      if (log_autoremove)
        return
          svn_error_quick_wrap 
          (err,
           _("Error running catastrophic recovery on hotcopy;  the \n"
             "DB_LOG_AUTOREMOVE feature may be interfering with the \n"
             "hotcopy algorithm.  If the problem persists, try deactivating \n"
             "this feature in DB_CONFIG"));
      else
        return err;
    }

  /* Only now that the hotcopied filesystem is complete,
     stamp it with a format file. */
  SVN_ERR(svn_io_write_version_file
          (svn_path_join(dest_path, FORMAT_FILE, pool), format, pool));

  if (clean_logs == TRUE)
    SVN_ERR(svn_fs_base__clean_logs(src_path, dest_path, pool));

  return SVN_NO_ERROR;
}



/* Deleting a Berkeley DB-based filesystem.  */


static svn_error_t *
base_delete_fs(const char *path,
               apr_pool_t *pool)
{
  /* First, use the Berkeley DB library function to remove any shared
     memory segments.  */
  SVN_ERR(svn_fs_bdb__remove(path, pool));

  /* Remove the environment directory. */
  SVN_ERR(svn_io_remove_dir(path, pool));

  return SVN_NO_ERROR;
}



/* Miscellany */

const char *
svn_fs_base__canonicalize_abspath(const char *path, apr_pool_t *pool)
{
  char *newpath;
  int path_len;
  int path_i = 0, newpath_i = 0;
  svn_boolean_t eating_slashes = FALSE;

  /* No PATH?  No problem. */
  if (! path)
    return NULL;
  
  /* Empty PATH?  That's just "/". */
  if (! *path)
    return apr_pstrdup(pool, "/");

  /* Now, the fun begins.  Alloc enough room to hold PATH with an
     added leading '/'. */
  path_len = strlen(path);
  newpath = apr_pcalloc(pool, path_len + 2);

  /* No leading slash?  Fix that. */
  if (*path != '/')
    {
      newpath[newpath_i++] = '/';
    }
  
  for (path_i = 0; path_i < path_len; path_i++)
    {
      if (path[path_i] == '/')
        {
          /* The current character is a '/'.  If we are eating up
             extra '/' characters, skip this character.  Else, note
             that we are now eating slashes. */
          if (eating_slashes)
            continue;
          eating_slashes = TRUE;
        }
      else
        {
          /* The current character is NOT a '/'.  If we were eating
             slashes, we need not do that any more. */
          if (eating_slashes)
            eating_slashes = FALSE;
        }

      /* Copy the current character into our new buffer. */
      newpath[newpath_i++] = path[path_i];
    }
  
  /* Did we leave a '/' attached to the end of NEWPATH (other than in
     the root directory case)? */
  if ((newpath[newpath_i - 1] == '/') && (newpath_i > 1))
    newpath[newpath_i - 1] = '\0';

  return newpath;
}

static const svn_version_t *
base_version(void)
{
  SVN_VERSION_BODY;
}

static const char *
base_get_description(void)
{
  return _("Module for working with a Berkeley DB repository.");
}



/* Base FS library vtable, used by the FS loader library. */
static fs_library_vtable_t library_vtable = {
  base_version,
  base_create,
  base_open,
  base_delete_fs,
  base_hotcopy,
  base_get_description,
  base_bdb_recover,
  base_bdb_logfiles,
  svn_fs_base__id_parse
};

svn_error_t *
svn_fs_base__init(const svn_version_t *loader_version,
                  fs_library_vtable_t **vtable)
{
  static const svn_version_checklist_t checklist[] =
    {
      { "svn_subr",  svn_subr_version },
      { "svn_delta", svn_delta_version },
      { NULL, NULL }
    };

  /* Simplified version check to make sure we can safely use the
     VTABLE parameter. The FS loader does a more exhaustive check. */
  if (loader_version->major != SVN_VER_MAJOR)
    return svn_error_createf(SVN_ERR_VERSION_MISMATCH, NULL,
                             _("Unsupported FS loader version (%d) for bdb"),
                             loader_version->major);
  SVN_ERR(svn_ver_check_list(base_version(), checklist));
  SVN_ERR(check_bdb_version());
  SVN_ERR(svn_fs_bdb__init());

  *vtable = &library_vtable;
  return SVN_NO_ERROR;
}
