/*
 * activity.c: DeltaV activity handling
 *
 * ====================================================================
 * Copyright (c) 2000-2004, 2008 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 <errno.h>

#include <apr_md5.h>

#include <httpd.h>
#include <mod_dav.h>

#include "svn_checksum.h"
#include "svn_error.h"
#include "svn_io.h"
#include "svn_path.h"
#include "svn_fs.h"
#include "svn_props.h"
#include "svn_repos.h"
#include "svn_pools.h"

#include "private/svn_fs_private.h"

#include "dav_svn.h"


/* Escape ACTIVITY_ID to be safely usable as a filename.  Simply
   returns the MD5 checksum of the id.
*/
static const char *
escape_activity(const char *activity_id, apr_pool_t *pool)
{
  svn_checksum_t *checksum;
  svn_error_clear(svn_checksum(&checksum, svn_checksum_md5, activity_id,
                               strlen(activity_id), pool));
  return svn_checksum_to_cstring_display(checksum, pool);
}

/* Return filename for ACTIVITY_ID under the repository in REPOS. */
static const char *
activity_pathname(const dav_svn_repos *repos, const char *activity_id)
{
  return svn_path_join(repos->activities_db,
                       escape_activity(activity_id, repos->pool),
                       repos->pool);
}

/* Return the transaction name of the activity stored in file
   PATHNAME, or NULL if PATHNAME cannot be read for any reason. */
static const char *
read_txn(const char *pathname, apr_pool_t *pool)
{
  apr_file_t *activity_file;
  apr_pool_t *iterpool = svn_pool_create(pool);
  apr_size_t len;
  svn_error_t *err = SVN_NO_ERROR;
  char *txn_name = apr_palloc(pool, SVN_FS__TXN_MAX_LEN+1);
  int i;

  /* Try up to 10 times to read the txn name, retrying on ESTALE
     (stale NFS file handle because of dav_svn__store_activity
     renaming the activity file into place).
   */
  for (i = 0; i < 10; i++)
    {
      svn_error_clear(err);
      svn_pool_clear(iterpool);

      err = svn_io_file_open(&activity_file, pathname,
                             APR_READ  | APR_BUFFERED,
                             APR_OS_DEFAULT, iterpool);
      if (err)
        {
#ifdef ESTALE
          if (APR_TO_OS_ERROR(err->apr_err) == ESTALE)
            /* Retry on ESTALE... */
            continue;
#endif
          /* ...else bail. */
          break;
        }

      len = SVN_FS__TXN_MAX_LEN;
      err = svn_io_read_length_line(activity_file, txn_name, &len, iterpool);
      if (err)
        {
#ifdef ESTALE
          if (APR_TO_OS_ERROR(err->apr_err) == ESTALE)
            continue;
#endif
          break;
        }

      err = svn_io_file_close(activity_file, iterpool);
#ifdef ESTALE
      if (err)
        {
          if (APR_TO_OS_ERROR(err->apr_err) == ESTALE)
            {
              /* No retry, just completely ignore this ESTALE. */
              svn_error_clear(err);
              err = SVN_NO_ERROR;
            }
        }
#endif

      /* We have a txn_name or had a non-ESTALE close failure; either
         way, we're finished. */
      break;
    }
  svn_pool_destroy(iterpool);

  /* ### let's just assume that any error means the
     ### activity/transaction doesn't exist */
  if (err)
    {
      svn_error_clear(err);
      return NULL;
    }

  return txn_name;
}


const char *
dav_svn__get_txn(const dav_svn_repos *repos, const char *activity_id)
{
  return read_txn(activity_pathname(repos, activity_id), repos->pool);
}


dav_error *
dav_svn__delete_activity(const dav_svn_repos *repos, const char *activity_id)
{
  dav_error *err = NULL;
  const char *pathname;
  svn_fs_txn_t *txn;
  const char *txn_name;
  svn_error_t *serr;

  /* gstein sez: If the activity ID is not in the database, return a
     404.  If the transaction is not present or is immutable, return a
     204.  For all other failures, return a 500. */

  pathname = activity_pathname(repos, activity_id);
  txn_name = read_txn(pathname, repos->pool);
  if (txn_name == NULL)
    {
      return dav_new_error(repos->pool, HTTP_NOT_FOUND, 0,
                           "could not find activity.");
    }

  /* After this point, we have to cleanup the value and database. */

  /* An empty txn_name indicates the transaction has been committed,
     so don't try to clean it up. */
  if (*txn_name)
    {
      /* Now, we attempt to delete TXN_NAME from the Subversion
         repository.  If we fail only because the transaction doesn't
         exist, don't sweat it (but then, also don't try to remove it). */
      if ((serr = svn_fs_open_txn(&txn, repos->fs, txn_name, repos->pool)))
        {
          if (serr->apr_err == SVN_ERR_FS_NO_SUCH_TRANSACTION)
            {
              svn_error_clear(serr);
              serr = SVN_NO_ERROR;
            }
          else
            {
              err = dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
                                         "could not open transaction.",
                                         repos->pool);
              return err;
            }
        }
      else
        {
          serr = svn_fs_abort_txn(txn, repos->pool);
          if (serr)
            {
              err = dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
                                         "could not abort transaction.",
                                         repos->pool);
              return err;
            }
        }
    }

  /* Finally, we remove the activity from the activities database. */
  serr = svn_io_remove_file(pathname, repos->pool);
  if (serr)
    err = dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
                               "unable to remove activity.",
                               repos->pool);

  return err;
}


dav_error *
dav_svn__store_activity(const dav_svn_repos *repos,
                        const char *activity_id,
                        const char *txn_name)
{
  const char *final_path;
  const char *tmp_path;
  const char *activity_contents;
  svn_error_t *err;

  /* Create activities directory if it does not yet exist. */
  err = svn_io_make_dir_recursively(repos->activities_db, repos->pool);
  if (err != NULL)
    return dav_svn__convert_err(err, HTTP_INTERNAL_SERVER_ERROR,
                                "could not initialize activity db.",
                                repos->pool);

  final_path = activity_pathname(repos, activity_id);

  activity_contents = apr_psprintf(repos->pool, "%s\n%s\n",
                                   txn_name, activity_id);

  /* ### is there another directory we already have and can write to? */
  err = svn_io_write_unique(&tmp_path,
                            svn_path_dirname(final_path, repos->pool),
                            activity_contents, strlen(activity_contents),
                            svn_io_file_del_none, repos->pool);
  if (err)
    {
      svn_error_t *serr = svn_error_quick_wrap(err,
                                               "Can't write activity db");

      /* Try to remove the tmp file, but we already have an error... */
      return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
                                  "could not write files.",
                                  repos->pool);
    }

  err = svn_io_file_rename(tmp_path, final_path, repos->pool);
  if (err)
    {
      svn_error_clear(svn_io_remove_file(tmp_path, repos->pool));
      return dav_svn__convert_err(err, HTTP_INTERNAL_SERVER_ERROR,
                                  "could not replace files.",
                                  repos->pool);
    }

  return NULL;
}


dav_error *
dav_svn__create_activity(const dav_svn_repos *repos,
                         const char **ptxn_name,
                         apr_pool_t *pool)
{
  svn_revnum_t rev;
  svn_fs_txn_t *txn;
  svn_error_t *serr;
  apr_hash_t *revprop_table = apr_hash_make(pool);

  if (repos->username)
    {
      apr_hash_set(revprop_table, SVN_PROP_REVISION_AUTHOR, APR_HASH_KEY_STRING,
                   svn_string_create(repos->username, pool));
    }

  serr = svn_fs_youngest_rev(&rev, repos->fs, pool);
  if (serr != NULL)
    {
      return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
                                  "could not determine youngest revision",
                                  repos->pool);
    }

  serr = svn_repos_fs_begin_txn_for_commit2(&txn, repos->repos, rev,
                                            revprop_table,
                                            repos->pool);
  if (serr != NULL)
    {
      return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
                                  "could not begin a transaction",
                                  repos->pool);
    }

  serr = svn_fs_txn_name(ptxn_name, txn, pool);
  if (serr != NULL)
    {
      return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
                                  "could not fetch transaction name",
                                  repos->pool);
    }

  return NULL;
}
