| /* fs_fs.c --- filesystem operations specific to fs_fs |
| * |
| * ==================================================================== |
| * 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 <stdlib.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <ctype.h> |
| #include <assert.h> |
| #include <errno.h> |
| |
| #include <apr_general.h> |
| #include <apr_pools.h> |
| #include <apr_file_io.h> |
| #include <apr_uuid.h> |
| #include <apr_lib.h> |
| #include <apr_md5.h> |
| #include <apr_sha1.h> |
| #include <apr_strings.h> |
| #include <apr_thread_mutex.h> |
| |
| #include "svn_pools.h" |
| #include "svn_fs.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_path.h" |
| #include "svn_checksum.h" |
| #include "svn_hash.h" |
| #include "svn_props.h" |
| #include "svn_sorts.h" |
| #include "svn_string.h" |
| #include "svn_time.h" |
| #include "svn_mergeinfo.h" |
| #include "svn_config.h" |
| #include "svn_ctype.h" |
| #include "svn_version.h" |
| |
| #include "fs.h" |
| #include "tree.h" |
| #include "lock.h" |
| #include "key-gen.h" |
| #include "fs_fs.h" |
| #include "id.h" |
| #include "rep-cache.h" |
| #include "temp_serializer.h" |
| |
| #include "private/svn_string_private.h" |
| #include "private/svn_fs_util.h" |
| #include "private/svn_subr_private.h" |
| #include "private/svn_delta_private.h" |
| #include "../libsvn_fs/fs-loader.h" |
| |
| #include "svn_private_config.h" |
| #include "temp_serializer.h" |
| |
| /* An arbitrary maximum path length, so clients can't run us out of memory |
| * by giving us arbitrarily large paths. */ |
| #define FSFS_MAX_PATH_LEN 4096 |
| |
| /* The default maximum number of files per directory to store in the |
| rev and revprops directory. The number below is somewhat arbitrary, |
| and can be overridden by defining the macro while compiling; the |
| figure of 1000 is reasonable for VFAT filesystems, which are by far |
| the worst performers in this area. */ |
| #ifndef SVN_FS_FS_DEFAULT_MAX_FILES_PER_DIR |
| #define SVN_FS_FS_DEFAULT_MAX_FILES_PER_DIR 1000 |
| #endif |
| |
| /* Begin deltification after a node history exceeded this this limit. |
| Useful values are 4 to 64 with 16 being a good compromise between |
| computational overhead and repository size savings. |
| Should be a power of 2. |
| Values < 2 will result in standard skip-delta behavior. */ |
| #define SVN_FS_FS_MAX_LINEAR_DELTIFICATION 16 |
| |
| /* Finding a deltification base takes operations proportional to the |
| number of changes being skipped. To prevent exploding runtime |
| during commits, limit the deltification range to this value. |
| Should be a power of 2 minus one. |
| Values < 1 disable deltification. */ |
| #define SVN_FS_FS_MAX_DELTIFICATION_WALK 1023 |
| |
| /* Give writing processes 10 seconds to replace an existing revprop |
| file with a new one. After that time, we assume that the writing |
| process got aborted and that we have re-read revprops. */ |
| #define REVPROP_CHANGE_TIMEOUT (10 * 1000000) |
| |
| /* The following are names of atomics that will be used to communicate |
| * revprop updates across all processes on this machine. */ |
| #define ATOMIC_REVPROP_GENERATION "rev-prop-generation" |
| #define ATOMIC_REVPROP_TIMEOUT "rev-prop-timeout" |
| #define ATOMIC_REVPROP_NAMESPACE "rev-prop-atomics" |
| |
| /* Following are defines that specify the textual elements of the |
| native filesystem directories and revision files. */ |
| |
| /* Headers used to describe node-revision in the revision file. */ |
| #define HEADER_ID "id" |
| #define HEADER_TYPE "type" |
| #define HEADER_COUNT "count" |
| #define HEADER_PROPS "props" |
| #define HEADER_TEXT "text" |
| #define HEADER_CPATH "cpath" |
| #define HEADER_PRED "pred" |
| #define HEADER_COPYFROM "copyfrom" |
| #define HEADER_COPYROOT "copyroot" |
| #define HEADER_FRESHTXNRT "is-fresh-txn-root" |
| #define HEADER_MINFO_HERE "minfo-here" |
| #define HEADER_MINFO_CNT "minfo-cnt" |
| |
| /* Kinds that a change can be. */ |
| #define ACTION_MODIFY "modify" |
| #define ACTION_ADD "add" |
| #define ACTION_DELETE "delete" |
| #define ACTION_REPLACE "replace" |
| #define ACTION_RESET "reset" |
| |
| /* True and False flags. */ |
| #define FLAG_TRUE "true" |
| #define FLAG_FALSE "false" |
| |
| /* Kinds that a node-rev can be. */ |
| #define KIND_FILE "file" |
| #define KIND_DIR "dir" |
| |
| /* Kinds of representation. */ |
| #define REP_PLAIN "PLAIN" |
| #define REP_DELTA "DELTA" |
| |
| /* Notes: |
| |
| To avoid opening and closing the rev-files all the time, it would |
| probably be advantageous to keep each rev-file open for the |
| lifetime of the transaction object. I'll leave that as a later |
| optimization for now. |
| |
| I didn't keep track of pool lifetimes at all in this code. There |
| are likely some errors because of that. |
| |
| */ |
| |
| /* The vtable associated with an open transaction object. */ |
| static txn_vtable_t txn_vtable = { |
| svn_fs_fs__commit_txn, |
| svn_fs_fs__abort_txn, |
| svn_fs_fs__txn_prop, |
| svn_fs_fs__txn_proplist, |
| svn_fs_fs__change_txn_prop, |
| svn_fs_fs__txn_root, |
| svn_fs_fs__change_txn_props |
| }; |
| |
| /* Declarations. */ |
| |
| static svn_error_t * |
| read_min_unpacked_rev(svn_revnum_t *min_unpacked_rev, |
| const char *path, |
| apr_pool_t *pool); |
| |
| static svn_error_t * |
| update_min_unpacked_rev(svn_fs_t *fs, apr_pool_t *pool); |
| |
| static svn_error_t * |
| get_youngest(svn_revnum_t *youngest_p, const char *fs_path, apr_pool_t *pool); |
| |
| static svn_error_t * |
| verify_walker(representation_t *rep, |
| void *baton, |
| svn_fs_t *fs, |
| apr_pool_t *scratch_pool); |
| |
| /* Pathname helper functions */ |
| |
| /* Return TRUE is REV is packed in FS, FALSE otherwise. */ |
| static svn_boolean_t |
| is_packed_rev(svn_fs_t *fs, svn_revnum_t rev) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| return (rev < ffd->min_unpacked_rev); |
| } |
| |
| /* Return TRUE is REV is packed in FS, FALSE otherwise. */ |
| static svn_boolean_t |
| is_packed_revprop(svn_fs_t *fs, svn_revnum_t rev) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* rev 0 will not be packed */ |
| return (rev < ffd->min_unpacked_rev) |
| && (rev != 0) |
| && (ffd->format >= SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT); |
| } |
| |
| static const char * |
| path_format(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| return svn_dirent_join(fs->path, PATH_FORMAT, pool); |
| } |
| |
| static APR_INLINE const char * |
| path_uuid(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| return svn_dirent_join(fs->path, PATH_UUID, pool); |
| } |
| |
| const char * |
| svn_fs_fs__path_current(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| return svn_dirent_join(fs->path, PATH_CURRENT, pool); |
| } |
| |
| static APR_INLINE const char * |
| path_txn_current(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| return svn_dirent_join(fs->path, PATH_TXN_CURRENT, pool); |
| } |
| |
| static APR_INLINE const char * |
| path_txn_current_lock(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| return svn_dirent_join(fs->path, PATH_TXN_CURRENT_LOCK, pool); |
| } |
| |
| static APR_INLINE const char * |
| path_lock(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| return svn_dirent_join(fs->path, PATH_LOCK_FILE, pool); |
| } |
| |
| static const char * |
| path_revprop_generation(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| return svn_dirent_join(fs->path, PATH_REVPROP_GENERATION, pool); |
| } |
| |
| static const char * |
| path_rev_packed(svn_fs_t *fs, svn_revnum_t rev, const char *kind, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| assert(ffd->max_files_per_dir); |
| assert(is_packed_rev(fs, rev)); |
| |
| return svn_dirent_join_many(pool, fs->path, PATH_REVS_DIR, |
| apr_psprintf(pool, |
| "%ld" PATH_EXT_PACKED_SHARD, |
| rev / ffd->max_files_per_dir), |
| kind, NULL); |
| } |
| |
| static const char * |
| path_rev_shard(svn_fs_t *fs, svn_revnum_t rev, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| assert(ffd->max_files_per_dir); |
| return svn_dirent_join_many(pool, fs->path, PATH_REVS_DIR, |
| apr_psprintf(pool, "%ld", |
| rev / ffd->max_files_per_dir), |
| NULL); |
| } |
| |
| static const char * |
| path_rev(svn_fs_t *fs, svn_revnum_t rev, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| assert(! is_packed_rev(fs, rev)); |
| |
| if (ffd->max_files_per_dir) |
| { |
| return svn_dirent_join(path_rev_shard(fs, rev, pool), |
| apr_psprintf(pool, "%ld", rev), |
| pool); |
| } |
| |
| return svn_dirent_join_many(pool, fs->path, PATH_REVS_DIR, |
| apr_psprintf(pool, "%ld", rev), NULL); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__path_rev_absolute(const char **path, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| if (ffd->format < SVN_FS_FS__MIN_PACKED_FORMAT |
| || ! is_packed_rev(fs, rev)) |
| { |
| *path = path_rev(fs, rev, pool); |
| } |
| else |
| { |
| *path = path_rev_packed(fs, rev, PATH_PACKED, pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static const char * |
| path_revprops_shard(svn_fs_t *fs, svn_revnum_t rev, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| assert(ffd->max_files_per_dir); |
| return svn_dirent_join_many(pool, fs->path, PATH_REVPROPS_DIR, |
| apr_psprintf(pool, "%ld", |
| rev / ffd->max_files_per_dir), |
| NULL); |
| } |
| |
| static const char * |
| path_revprops_pack_shard(svn_fs_t *fs, svn_revnum_t rev, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| assert(ffd->max_files_per_dir); |
| return svn_dirent_join_many(pool, fs->path, PATH_REVPROPS_DIR, |
| apr_psprintf(pool, "%ld" PATH_EXT_PACKED_SHARD, |
| rev / ffd->max_files_per_dir), |
| NULL); |
| } |
| |
| static const char * |
| path_revprops(svn_fs_t *fs, svn_revnum_t rev, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| if (ffd->max_files_per_dir) |
| { |
| return svn_dirent_join(path_revprops_shard(fs, rev, pool), |
| apr_psprintf(pool, "%ld", rev), |
| pool); |
| } |
| |
| return svn_dirent_join_many(pool, fs->path, PATH_REVPROPS_DIR, |
| apr_psprintf(pool, "%ld", rev), NULL); |
| } |
| |
| static APR_INLINE const char * |
| path_txn_dir(svn_fs_t *fs, const char *txn_id, apr_pool_t *pool) |
| { |
| SVN_ERR_ASSERT_NO_RETURN(txn_id != NULL); |
| return svn_dirent_join_many(pool, fs->path, PATH_TXNS_DIR, |
| apr_pstrcat(pool, txn_id, PATH_EXT_TXN, |
| (char *)NULL), |
| NULL); |
| } |
| |
| /* Return the name of the sha1->rep mapping file in transaction TXN_ID |
| * within FS for the given SHA1 checksum. Use POOL for allocations. |
| */ |
| static APR_INLINE const char * |
| path_txn_sha1(svn_fs_t *fs, const char *txn_id, svn_checksum_t *sha1, |
| apr_pool_t *pool) |
| { |
| return svn_dirent_join(path_txn_dir(fs, txn_id, pool), |
| svn_checksum_to_cstring(sha1, pool), |
| pool); |
| } |
| |
| static APR_INLINE const char * |
| path_txn_changes(svn_fs_t *fs, const char *txn_id, apr_pool_t *pool) |
| { |
| return svn_dirent_join(path_txn_dir(fs, txn_id, pool), PATH_CHANGES, pool); |
| } |
| |
| static APR_INLINE const char * |
| path_txn_props(svn_fs_t *fs, const char *txn_id, apr_pool_t *pool) |
| { |
| return svn_dirent_join(path_txn_dir(fs, txn_id, pool), PATH_TXN_PROPS, pool); |
| } |
| |
| static APR_INLINE const char * |
| path_txn_next_ids(svn_fs_t *fs, const char *txn_id, apr_pool_t *pool) |
| { |
| return svn_dirent_join(path_txn_dir(fs, txn_id, pool), PATH_NEXT_IDS, pool); |
| } |
| |
| static APR_INLINE const char * |
| path_min_unpacked_rev(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| return svn_dirent_join(fs->path, PATH_MIN_UNPACKED_REV, pool); |
| } |
| |
| |
| static APR_INLINE const char * |
| path_txn_proto_rev(svn_fs_t *fs, const char *txn_id, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| if (ffd->format >= SVN_FS_FS__MIN_PROTOREVS_DIR_FORMAT) |
| return svn_dirent_join_many(pool, fs->path, PATH_TXN_PROTOS_DIR, |
| apr_pstrcat(pool, txn_id, PATH_EXT_REV, |
| (char *)NULL), |
| NULL); |
| else |
| return svn_dirent_join(path_txn_dir(fs, txn_id, pool), PATH_REV, pool); |
| } |
| |
| static APR_INLINE const char * |
| path_txn_proto_rev_lock(svn_fs_t *fs, const char *txn_id, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| if (ffd->format >= SVN_FS_FS__MIN_PROTOREVS_DIR_FORMAT) |
| return svn_dirent_join_many(pool, fs->path, PATH_TXN_PROTOS_DIR, |
| apr_pstrcat(pool, txn_id, PATH_EXT_REV_LOCK, |
| (char *)NULL), |
| NULL); |
| else |
| return svn_dirent_join(path_txn_dir(fs, txn_id, pool), PATH_REV_LOCK, |
| pool); |
| } |
| |
| static const char * |
| path_txn_node_rev(svn_fs_t *fs, const svn_fs_id_t *id, apr_pool_t *pool) |
| { |
| const char *txn_id = svn_fs_fs__id_txn_id(id); |
| const char *node_id = svn_fs_fs__id_node_id(id); |
| const char *copy_id = svn_fs_fs__id_copy_id(id); |
| const char *name = apr_psprintf(pool, PATH_PREFIX_NODE "%s.%s", |
| node_id, copy_id); |
| |
| return svn_dirent_join(path_txn_dir(fs, txn_id, pool), name, pool); |
| } |
| |
| static APR_INLINE const char * |
| path_txn_node_props(svn_fs_t *fs, const svn_fs_id_t *id, apr_pool_t *pool) |
| { |
| return apr_pstrcat(pool, path_txn_node_rev(fs, id, pool), PATH_EXT_PROPS, |
| (char *)NULL); |
| } |
| |
| static APR_INLINE const char * |
| path_txn_node_children(svn_fs_t *fs, const svn_fs_id_t *id, apr_pool_t *pool) |
| { |
| return apr_pstrcat(pool, path_txn_node_rev(fs, id, pool), |
| PATH_EXT_CHILDREN, (char *)NULL); |
| } |
| |
| static APR_INLINE const char * |
| path_node_origin(svn_fs_t *fs, const char *node_id, apr_pool_t *pool) |
| { |
| size_t len = strlen(node_id); |
| const char *node_id_minus_last_char = |
| (len == 1) ? "0" : apr_pstrmemdup(pool, node_id, len - 1); |
| return svn_dirent_join_many(pool, fs->path, PATH_NODE_ORIGINS_DIR, |
| node_id_minus_last_char, NULL); |
| } |
| |
| static APR_INLINE const char * |
| path_and_offset_of(apr_file_t *file, apr_pool_t *pool) |
| { |
| const char *path; |
| apr_off_t offset = 0; |
| |
| if (apr_file_name_get(&path, file) != APR_SUCCESS) |
| path = "(unknown)"; |
| |
| if (apr_file_seek(file, APR_CUR, &offset) != APR_SUCCESS) |
| offset = -1; |
| |
| return apr_psprintf(pool, "%s:%" APR_OFF_T_FMT, path, offset); |
| } |
| |
| |
| |
| /* Functions for working with shared transaction data. */ |
| |
| /* Return the transaction object for transaction TXN_ID from the |
| transaction list of filesystem FS (which must already be locked via the |
| txn_list_lock mutex). If the transaction does not exist in the list, |
| then create a new transaction object and return it (if CREATE_NEW is |
| true) or return NULL (otherwise). */ |
| static fs_fs_shared_txn_data_t * |
| get_shared_txn(svn_fs_t *fs, const char *txn_id, svn_boolean_t create_new) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| fs_fs_shared_data_t *ffsd = ffd->shared; |
| fs_fs_shared_txn_data_t *txn; |
| |
| for (txn = ffsd->txns; txn; txn = txn->next) |
| if (strcmp(txn->txn_id, txn_id) == 0) |
| break; |
| |
| if (txn || !create_new) |
| return txn; |
| |
| /* Use the transaction object from the (single-object) freelist, |
| if one is available, or otherwise create a new object. */ |
| if (ffsd->free_txn) |
| { |
| txn = ffsd->free_txn; |
| ffsd->free_txn = NULL; |
| } |
| else |
| { |
| apr_pool_t *subpool = svn_pool_create(ffsd->common_pool); |
| txn = apr_palloc(subpool, sizeof(*txn)); |
| txn->pool = subpool; |
| } |
| |
| assert(strlen(txn_id) < sizeof(txn->txn_id)); |
| apr_cpystrn(txn->txn_id, txn_id, sizeof(txn->txn_id)); |
| txn->being_written = FALSE; |
| |
| /* Link this transaction into the head of the list. We will typically |
| be dealing with only one active transaction at a time, so it makes |
| sense for searches through the transaction list to look at the |
| newest transactions first. */ |
| txn->next = ffsd->txns; |
| ffsd->txns = txn; |
| |
| return txn; |
| } |
| |
| /* Free the transaction object for transaction TXN_ID, and remove it |
| from the transaction list of filesystem FS (which must already be |
| locked via the txn_list_lock mutex). Do nothing if the transaction |
| does not exist. */ |
| static void |
| free_shared_txn(svn_fs_t *fs, const char *txn_id) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| fs_fs_shared_data_t *ffsd = ffd->shared; |
| fs_fs_shared_txn_data_t *txn, *prev = NULL; |
| |
| for (txn = ffsd->txns; txn; prev = txn, txn = txn->next) |
| if (strcmp(txn->txn_id, txn_id) == 0) |
| break; |
| |
| if (!txn) |
| return; |
| |
| if (prev) |
| prev->next = txn->next; |
| else |
| ffsd->txns = txn->next; |
| |
| /* As we typically will be dealing with one transaction after another, |
| we will maintain a single-object free list so that we can hopefully |
| keep reusing the same transaction object. */ |
| if (!ffsd->free_txn) |
| ffsd->free_txn = txn; |
| else |
| svn_pool_destroy(txn->pool); |
| } |
| |
| |
| /* Obtain a lock on the transaction list of filesystem FS, call BODY |
| with FS, BATON, and POOL, and then unlock the transaction list. |
| Return what BODY returned. */ |
| static svn_error_t * |
| with_txnlist_lock(svn_fs_t *fs, |
| svn_error_t *(*body)(svn_fs_t *fs, |
| const void *baton, |
| apr_pool_t *pool), |
| const void *baton, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| fs_fs_shared_data_t *ffsd = ffd->shared; |
| |
| SVN_MUTEX__WITH_LOCK(ffsd->txn_list_lock, |
| body(fs, baton, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Get a lock on empty file LOCK_FILENAME, creating it in POOL. */ |
| static svn_error_t * |
| get_lock_on_filesystem(const char *lock_filename, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err = svn_io_file_lock2(lock_filename, TRUE, FALSE, pool); |
| |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| /* No lock file? No big deal; these are just empty files |
| anyway. Create it and try again. */ |
| svn_error_clear(err); |
| err = NULL; |
| |
| SVN_ERR(svn_io_file_create(lock_filename, "", pool)); |
| SVN_ERR(svn_io_file_lock2(lock_filename, TRUE, FALSE, pool)); |
| } |
| |
| return svn_error_trace(err); |
| } |
| |
| /* Reset the HAS_WRITE_LOCK member in the FFD given as BATON_VOID. |
| When registered with the pool holding the lock on the lock file, |
| this makes sure the flag gets reset just before we release the lock. */ |
| static apr_status_t |
| reset_lock_flag(void *baton_void) |
| { |
| fs_fs_data_t *ffd = baton_void; |
| ffd->has_write_lock = FALSE; |
| return APR_SUCCESS; |
| } |
| |
| /* Obtain a write lock on the file LOCK_FILENAME (protecting with |
| LOCK_MUTEX if APR is threaded) in a subpool of POOL, call BODY with |
| BATON and that subpool, destroy the subpool (releasing the write |
| lock) and return what BODY returned. If IS_GLOBAL_LOCK is set, |
| set the HAS_WRITE_LOCK flag while we keep the write lock. */ |
| static svn_error_t * |
| with_some_lock_file(svn_fs_t *fs, |
| svn_error_t *(*body)(void *baton, |
| apr_pool_t *pool), |
| void *baton, |
| const char *lock_filename, |
| svn_boolean_t is_global_lock, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *subpool = svn_pool_create(pool); |
| svn_error_t *err = get_lock_on_filesystem(lock_filename, subpool); |
| |
| if (!err) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| if (is_global_lock) |
| { |
| /* set the "got the lock" flag and register reset function */ |
| apr_pool_cleanup_register(subpool, |
| ffd, |
| reset_lock_flag, |
| apr_pool_cleanup_null); |
| ffd->has_write_lock = TRUE; |
| } |
| |
| /* nobody else will modify the repo state |
| => read HEAD & pack info once */ |
| if (ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) |
| SVN_ERR(update_min_unpacked_rev(fs, pool)); |
| SVN_ERR(get_youngest(&ffd->youngest_rev_cache, fs->path, |
| pool)); |
| err = body(baton, subpool); |
| } |
| |
| svn_pool_destroy(subpool); |
| |
| return svn_error_trace(err); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__with_write_lock(svn_fs_t *fs, |
| svn_error_t *(*body)(void *baton, |
| apr_pool_t *pool), |
| void *baton, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| fs_fs_shared_data_t *ffsd = ffd->shared; |
| |
| SVN_MUTEX__WITH_LOCK(ffsd->fs_write_lock, |
| with_some_lock_file(fs, body, baton, |
| path_lock(fs, pool), |
| TRUE, |
| pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Run BODY (with BATON and POOL) while the txn-current file |
| of FS is locked. */ |
| static svn_error_t * |
| with_txn_current_lock(svn_fs_t *fs, |
| svn_error_t *(*body)(void *baton, |
| apr_pool_t *pool), |
| void *baton, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| fs_fs_shared_data_t *ffsd = ffd->shared; |
| |
| SVN_MUTEX__WITH_LOCK(ffsd->txn_current_lock, |
| with_some_lock_file(fs, body, baton, |
| path_txn_current_lock(fs, pool), |
| FALSE, |
| pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* A structure used by unlock_proto_rev() and unlock_proto_rev_body(), |
| which see. */ |
| struct unlock_proto_rev_baton |
| { |
| const char *txn_id; |
| void *lockcookie; |
| }; |
| |
| /* Callback used in the implementation of unlock_proto_rev(). */ |
| static svn_error_t * |
| unlock_proto_rev_body(svn_fs_t *fs, const void *baton, apr_pool_t *pool) |
| { |
| const struct unlock_proto_rev_baton *b = baton; |
| const char *txn_id = b->txn_id; |
| apr_file_t *lockfile = b->lockcookie; |
| fs_fs_shared_txn_data_t *txn = get_shared_txn(fs, txn_id, FALSE); |
| apr_status_t apr_err; |
| |
| if (!txn) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Can't unlock unknown transaction '%s'"), |
| txn_id); |
| if (!txn->being_written) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Can't unlock nonlocked transaction '%s'"), |
| txn_id); |
| |
| apr_err = apr_file_unlock(lockfile); |
| if (apr_err) |
| return svn_error_wrap_apr |
| (apr_err, |
| _("Can't unlock prototype revision lockfile for transaction '%s'"), |
| txn_id); |
| apr_err = apr_file_close(lockfile); |
| if (apr_err) |
| return svn_error_wrap_apr |
| (apr_err, |
| _("Can't close prototype revision lockfile for transaction '%s'"), |
| txn_id); |
| |
| txn->being_written = FALSE; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Unlock the prototype revision file for transaction TXN_ID in filesystem |
| FS using cookie LOCKCOOKIE. The original prototype revision file must |
| have been closed _before_ calling this function. |
| |
| Perform temporary allocations in POOL. */ |
| static svn_error_t * |
| unlock_proto_rev(svn_fs_t *fs, const char *txn_id, void *lockcookie, |
| apr_pool_t *pool) |
| { |
| struct unlock_proto_rev_baton b; |
| |
| b.txn_id = txn_id; |
| b.lockcookie = lockcookie; |
| return with_txnlist_lock(fs, unlock_proto_rev_body, &b, pool); |
| } |
| |
| /* Same as unlock_proto_rev(), but requires that the transaction list |
| lock is already held. */ |
| static svn_error_t * |
| unlock_proto_rev_list_locked(svn_fs_t *fs, const char *txn_id, |
| void *lockcookie, |
| apr_pool_t *pool) |
| { |
| struct unlock_proto_rev_baton b; |
| |
| b.txn_id = txn_id; |
| b.lockcookie = lockcookie; |
| return unlock_proto_rev_body(fs, &b, pool); |
| } |
| |
| /* A structure used by get_writable_proto_rev() and |
| get_writable_proto_rev_body(), which see. */ |
| struct get_writable_proto_rev_baton |
| { |
| apr_file_t **file; |
| void **lockcookie; |
| const char *txn_id; |
| }; |
| |
| /* Callback used in the implementation of get_writable_proto_rev(). */ |
| static svn_error_t * |
| get_writable_proto_rev_body(svn_fs_t *fs, const void *baton, apr_pool_t *pool) |
| { |
| const struct get_writable_proto_rev_baton *b = baton; |
| apr_file_t **file = b->file; |
| void **lockcookie = b->lockcookie; |
| const char *txn_id = b->txn_id; |
| svn_error_t *err; |
| fs_fs_shared_txn_data_t *txn = get_shared_txn(fs, txn_id, TRUE); |
| |
| /* First, ensure that no thread in this process (including this one) |
| is currently writing to this transaction's proto-rev file. */ |
| if (txn->being_written) |
| return svn_error_createf(SVN_ERR_FS_REP_BEING_WRITTEN, NULL, |
| _("Cannot write to the prototype revision file " |
| "of transaction '%s' because a previous " |
| "representation is currently being written by " |
| "this process"), |
| txn_id); |
| |
| |
| /* We know that no thread in this process is writing to the proto-rev |
| file, and by extension, that no thread in this process is holding a |
| lock on the prototype revision lock file. It is therefore safe |
| for us to attempt to lock this file, to see if any other process |
| is holding a lock. */ |
| |
| { |
| apr_file_t *lockfile; |
| apr_status_t apr_err; |
| const char *lockfile_path = path_txn_proto_rev_lock(fs, txn_id, pool); |
| |
| /* Open the proto-rev lockfile, creating it if necessary, as it may |
| not exist if the transaction dates from before the lockfiles were |
| introduced. |
| |
| ### We'd also like to use something like svn_io_file_lock2(), but |
| that forces us to create a subpool just to be able to unlock |
| the file, which seems a waste. */ |
| SVN_ERR(svn_io_file_open(&lockfile, lockfile_path, |
| APR_WRITE | APR_CREATE, APR_OS_DEFAULT, pool)); |
| |
| apr_err = apr_file_lock(lockfile, |
| APR_FLOCK_EXCLUSIVE | APR_FLOCK_NONBLOCK); |
| if (apr_err) |
| { |
| svn_error_clear(svn_io_file_close(lockfile, pool)); |
| |
| if (APR_STATUS_IS_EAGAIN(apr_err)) |
| return svn_error_createf(SVN_ERR_FS_REP_BEING_WRITTEN, NULL, |
| _("Cannot write to the prototype revision " |
| "file of transaction '%s' because a " |
| "previous representation is currently " |
| "being written by another process"), |
| txn_id); |
| |
| return svn_error_wrap_apr(apr_err, |
| _("Can't get exclusive lock on file '%s'"), |
| svn_dirent_local_style(lockfile_path, pool)); |
| } |
| |
| *lockcookie = lockfile; |
| } |
| |
| /* We've successfully locked the transaction; mark it as such. */ |
| txn->being_written = TRUE; |
| |
| |
| /* Now open the prototype revision file and seek to the end. */ |
| err = svn_io_file_open(file, path_txn_proto_rev(fs, txn_id, pool), |
| APR_WRITE | APR_BUFFERED, APR_OS_DEFAULT, pool); |
| |
| /* You might expect that we could dispense with the following seek |
| and achieve the same thing by opening the file using APR_APPEND. |
| Unfortunately, APR's buffered file implementation unconditionally |
| places its initial file pointer at the start of the file (even for |
| files opened with APR_APPEND), so we need this seek to reconcile |
| the APR file pointer to the OS file pointer (since we need to be |
| able to read the current file position later). */ |
| if (!err) |
| { |
| apr_off_t offset = 0; |
| err = svn_io_file_seek(*file, APR_END, &offset, pool); |
| } |
| |
| if (err) |
| { |
| err = svn_error_compose_create( |
| err, |
| unlock_proto_rev_list_locked(fs, txn_id, *lockcookie, pool)); |
| |
| *lockcookie = NULL; |
| } |
| |
| return svn_error_trace(err); |
| } |
| |
| /* Get a handle to the prototype revision file for transaction TXN_ID in |
| filesystem FS, and lock it for writing. Return FILE, a file handle |
| positioned at the end of the file, and LOCKCOOKIE, a cookie that |
| should be passed to unlock_proto_rev() to unlock the file once FILE |
| has been closed. |
| |
| If the prototype revision file is already locked, return error |
| SVN_ERR_FS_REP_BEING_WRITTEN. |
| |
| Perform all allocations in POOL. */ |
| static svn_error_t * |
| get_writable_proto_rev(apr_file_t **file, |
| void **lockcookie, |
| svn_fs_t *fs, const char *txn_id, |
| apr_pool_t *pool) |
| { |
| struct get_writable_proto_rev_baton b; |
| |
| b.file = file; |
| b.lockcookie = lockcookie; |
| b.txn_id = txn_id; |
| |
| return with_txnlist_lock(fs, get_writable_proto_rev_body, &b, pool); |
| } |
| |
| /* Callback used in the implementation of purge_shared_txn(). */ |
| static svn_error_t * |
| purge_shared_txn_body(svn_fs_t *fs, const void *baton, apr_pool_t *pool) |
| { |
| const char *txn_id = baton; |
| |
| free_shared_txn(fs, txn_id); |
| svn_fs_fs__reset_txn_caches(fs); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Purge the shared data for transaction TXN_ID in filesystem FS. |
| Perform all allocations in POOL. */ |
| static svn_error_t * |
| purge_shared_txn(svn_fs_t *fs, const char *txn_id, apr_pool_t *pool) |
| { |
| return with_txnlist_lock(fs, purge_shared_txn_body, txn_id, pool); |
| } |
| |
| |
| |
| /* Fetch the current offset of FILE into *OFFSET_P. */ |
| static svn_error_t * |
| get_file_offset(apr_off_t *offset_p, apr_file_t *file, apr_pool_t *pool) |
| { |
| apr_off_t offset; |
| |
| /* Note that, for buffered files, one (possibly surprising) side-effect |
| of this call is to flush any unwritten data to disk. */ |
| offset = 0; |
| SVN_ERR(svn_io_file_seek(file, APR_CUR, &offset, pool)); |
| *offset_p = offset; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Check that BUF, a nul-terminated buffer of text from file PATH, |
| contains only digits at OFFSET and beyond, raising an error if not. |
| TITLE contains a user-visible description of the file, usually the |
| short file name. |
| |
| Uses POOL for temporary allocation. */ |
| static svn_error_t * |
| check_file_buffer_numeric(const char *buf, apr_off_t offset, |
| const char *path, const char *title, |
| apr_pool_t *pool) |
| { |
| const char *p; |
| |
| for (p = buf + offset; *p; p++) |
| if (!svn_ctype_isdigit(*p)) |
| return svn_error_createf(SVN_ERR_BAD_VERSION_FILE_FORMAT, NULL, |
| _("%s file '%s' contains unexpected non-digit '%c' within '%s'"), |
| title, svn_dirent_local_style(path, pool), *p, buf); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Check that BUF, a nul-terminated buffer of text from format file PATH, |
| contains only digits at OFFSET and beyond, raising an error if not. |
| |
| Uses POOL for temporary allocation. */ |
| static svn_error_t * |
| check_format_file_buffer_numeric(const char *buf, apr_off_t offset, |
| const char *path, apr_pool_t *pool) |
| { |
| return check_file_buffer_numeric(buf, offset, path, "Format", pool); |
| } |
| |
| /* Return the error SVN_ERR_FS_UNSUPPORTED_FORMAT if FS's format |
| number is not the same as a format number supported by this |
| Subversion. */ |
| static svn_error_t * |
| check_format(int format) |
| { |
| /* Blacklist. These formats may be either younger or older than |
| SVN_FS_FS__FORMAT_NUMBER, but we don't support them. */ |
| if (format == SVN_FS_FS__PACKED_REVPROP_SQLITE_DEV_FORMAT) |
| return svn_error_createf(SVN_ERR_FS_UNSUPPORTED_FORMAT, NULL, |
| _("Found format '%d', only created by " |
| "unreleased dev builds; see " |
| "http://subversion.apache.org" |
| "/docs/release-notes/1.7#revprop-packing"), |
| format); |
| |
| /* We support all formats from 1-current simultaneously */ |
| if (1 <= format && format <= SVN_FS_FS__FORMAT_NUMBER) |
| return SVN_NO_ERROR; |
| |
| return svn_error_createf(SVN_ERR_FS_UNSUPPORTED_FORMAT, NULL, |
| _("Expected FS format between '1' and '%d'; found format '%d'"), |
| SVN_FS_FS__FORMAT_NUMBER, format); |
| } |
| |
| /* Read the format number and maximum number of files per directory |
| from PATH and return them in *PFORMAT and *MAX_FILES_PER_DIR |
| respectively. |
| |
| *MAX_FILES_PER_DIR is obtained from the 'layout' format option, and |
| will be set to zero if a linear scheme should be used. |
| |
| Use POOL for temporary allocation. */ |
| static svn_error_t * |
| read_format(int *pformat, int *max_files_per_dir, |
| const char *path, apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| svn_stream_t *stream; |
| svn_stringbuf_t *content; |
| svn_stringbuf_t *buf; |
| svn_boolean_t eos = FALSE; |
| |
| err = svn_stringbuf_from_file2(&content, path, pool); |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| /* Treat an absent format file as format 1. Do not try to |
| create the format file on the fly, because the repository |
| might be read-only for us, or this might be a read-only |
| operation, and the spirit of FSFS is to make no changes |
| whatseover in read-only operations. See thread starting at |
| http://subversion.tigris.org/servlets/ReadMsg?list=dev&msgNo=97600 |
| for more. */ |
| svn_error_clear(err); |
| *pformat = 1; |
| *max_files_per_dir = 0; |
| |
| return SVN_NO_ERROR; |
| } |
| SVN_ERR(err); |
| |
| stream = svn_stream_from_stringbuf(content, pool); |
| SVN_ERR(svn_stream_readline(stream, &buf, "\n", &eos, pool)); |
| if (buf->len == 0 && eos) |
| { |
| /* Return a more useful error message. */ |
| return svn_error_createf(SVN_ERR_BAD_VERSION_FILE_FORMAT, NULL, |
| _("Can't read first line of format file '%s'"), |
| svn_dirent_local_style(path, pool)); |
| } |
| |
| /* Check that the first line contains only digits. */ |
| SVN_ERR(check_format_file_buffer_numeric(buf->data, 0, path, pool)); |
| SVN_ERR(svn_cstring_atoi(pformat, buf->data)); |
| |
| /* Check that we support this format at all */ |
| SVN_ERR(check_format(*pformat)); |
| |
| /* Set the default values for anything that can be set via an option. */ |
| *max_files_per_dir = 0; |
| |
| /* Read any options. */ |
| while (!eos) |
| { |
| SVN_ERR(svn_stream_readline(stream, &buf, "\n", &eos, pool)); |
| if (buf->len == 0) |
| break; |
| |
| if (*pformat >= SVN_FS_FS__MIN_LAYOUT_FORMAT_OPTION_FORMAT && |
| strncmp(buf->data, "layout ", 7) == 0) |
| { |
| if (strcmp(buf->data + 7, "linear") == 0) |
| { |
| *max_files_per_dir = 0; |
| continue; |
| } |
| |
| if (strncmp(buf->data + 7, "sharded ", 8) == 0) |
| { |
| /* Check that the argument is numeric. */ |
| SVN_ERR(check_format_file_buffer_numeric(buf->data, 15, path, pool)); |
| SVN_ERR(svn_cstring_atoi(max_files_per_dir, buf->data + 15)); |
| continue; |
| } |
| } |
| |
| return svn_error_createf(SVN_ERR_BAD_VERSION_FILE_FORMAT, NULL, |
| _("'%s' contains invalid filesystem format option '%s'"), |
| svn_dirent_local_style(path, pool), buf->data); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write the format number and maximum number of files per directory |
| to a new format file in PATH, possibly expecting to overwrite a |
| previously existing file. |
| |
| Use POOL for temporary allocation. */ |
| static svn_error_t * |
| write_format(const char *path, int format, int max_files_per_dir, |
| svn_boolean_t overwrite, apr_pool_t *pool) |
| { |
| svn_stringbuf_t *sb; |
| |
| SVN_ERR_ASSERT(1 <= format && format <= SVN_FS_FS__FORMAT_NUMBER); |
| |
| sb = svn_stringbuf_createf(pool, "%d\n", format); |
| |
| if (format >= SVN_FS_FS__MIN_LAYOUT_FORMAT_OPTION_FORMAT) |
| { |
| if (max_files_per_dir) |
| svn_stringbuf_appendcstr(sb, apr_psprintf(pool, "layout sharded %d\n", |
| max_files_per_dir)); |
| else |
| svn_stringbuf_appendcstr(sb, "layout linear\n"); |
| } |
| |
| /* svn_io_write_version_file() does a load of magic to allow it to |
| replace version files that already exist. We only need to do |
| that when we're allowed to overwrite an existing file. */ |
| if (! overwrite) |
| { |
| /* Create the file */ |
| SVN_ERR(svn_io_file_create(path, sb->data, pool)); |
| } |
| else |
| { |
| const char *path_tmp; |
| |
| SVN_ERR(svn_io_write_unique(&path_tmp, |
| svn_dirent_dirname(path, pool), |
| sb->data, sb->len, |
| svn_io_file_del_none, pool)); |
| |
| /* rename the temp file as the real destination */ |
| SVN_ERR(svn_io_file_rename(path_tmp, path, pool)); |
| } |
| |
| /* And set the perms to make it read only */ |
| return svn_io_set_file_read_only(path, FALSE, pool); |
| } |
| |
| svn_boolean_t |
| svn_fs_fs__fs_supports_mergeinfo(svn_fs_t *fs) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| return ffd->format >= SVN_FS_FS__MIN_MERGEINFO_FORMAT; |
| } |
| |
| /* Read the configuration information of the file system at FS_PATH |
| * and set the respective values in FFD. Use POOL for allocations. |
| */ |
| static svn_error_t * |
| read_config(fs_fs_data_t *ffd, |
| const char *fs_path, |
| apr_pool_t *pool) |
| { |
| SVN_ERR(svn_config_read3(&ffd->config, |
| svn_dirent_join(fs_path, PATH_CONFIG, pool), |
| FALSE, FALSE, FALSE, pool)); |
| |
| /* Initialize ffd->rep_sharing_allowed. */ |
| if (ffd->format >= SVN_FS_FS__MIN_REP_SHARING_FORMAT) |
| SVN_ERR(svn_config_get_bool(ffd->config, &ffd->rep_sharing_allowed, |
| CONFIG_SECTION_REP_SHARING, |
| CONFIG_OPTION_ENABLE_REP_SHARING, TRUE)); |
| else |
| ffd->rep_sharing_allowed = FALSE; |
| |
| /* Initialize deltification settings in ffd. */ |
| if (ffd->format >= SVN_FS_FS__MIN_DELTIFICATION_FORMAT) |
| { |
| SVN_ERR(svn_config_get_bool(ffd->config, &ffd->deltify_directories, |
| CONFIG_SECTION_DELTIFICATION, |
| CONFIG_OPTION_ENABLE_DIR_DELTIFICATION, |
| FALSE)); |
| SVN_ERR(svn_config_get_bool(ffd->config, &ffd->deltify_properties, |
| CONFIG_SECTION_DELTIFICATION, |
| CONFIG_OPTION_ENABLE_PROPS_DELTIFICATION, |
| FALSE)); |
| SVN_ERR(svn_config_get_int64(ffd->config, &ffd->max_deltification_walk, |
| CONFIG_SECTION_DELTIFICATION, |
| CONFIG_OPTION_MAX_DELTIFICATION_WALK, |
| SVN_FS_FS_MAX_DELTIFICATION_WALK)); |
| SVN_ERR(svn_config_get_int64(ffd->config, &ffd->max_linear_deltification, |
| CONFIG_SECTION_DELTIFICATION, |
| CONFIG_OPTION_MAX_LINEAR_DELTIFICATION, |
| SVN_FS_FS_MAX_LINEAR_DELTIFICATION)); |
| } |
| else |
| { |
| ffd->deltify_directories = FALSE; |
| ffd->deltify_properties = FALSE; |
| ffd->max_deltification_walk = SVN_FS_FS_MAX_DELTIFICATION_WALK; |
| ffd->max_linear_deltification = SVN_FS_FS_MAX_LINEAR_DELTIFICATION; |
| } |
| |
| /* Initialize revprop packing settings in ffd. */ |
| if (ffd->format >= SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT) |
| { |
| SVN_ERR(svn_config_get_bool(ffd->config, &ffd->compress_packed_revprops, |
| CONFIG_SECTION_PACKED_REVPROPS, |
| CONFIG_OPTION_COMPRESS_PACKED_REVPROPS, |
| FALSE)); |
| SVN_ERR(svn_config_get_int64(ffd->config, &ffd->revprop_pack_size, |
| CONFIG_SECTION_PACKED_REVPROPS, |
| CONFIG_OPTION_REVPROP_PACK_SIZE, |
| ffd->compress_packed_revprops |
| ? 0x100 |
| : 0x40)); |
| |
| ffd->revprop_pack_size *= 1024; |
| } |
| else |
| { |
| ffd->revprop_pack_size = 0x10000; |
| ffd->compress_packed_revprops = FALSE; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| write_config(svn_fs_t *fs, |
| apr_pool_t *pool) |
| { |
| #define NL APR_EOL_STR |
| static const char * const fsfs_conf_contents = |
| "### This file controls the configuration of the FSFS filesystem." NL |
| "" NL |
| "[" SVN_CACHE_CONFIG_CATEGORY_MEMCACHED_SERVERS "]" NL |
| "### These options name memcached servers used to cache internal FSFS" NL |
| "### data. See http://www.danga.com/memcached/ for more information on" NL |
| "### memcached. To use memcached with FSFS, run one or more memcached" NL |
| "### servers, and specify each of them as an option like so:" NL |
| "# first-server = 127.0.0.1:11211" NL |
| "# remote-memcached = mymemcached.corp.example.com:11212" NL |
| "### The option name is ignored; the value is of the form HOST:PORT." NL |
| "### memcached servers can be shared between multiple repositories;" NL |
| "### however, if you do this, you *must* ensure that repositories have" NL |
| "### distinct UUIDs and paths, or else cached data from one repository" NL |
| "### might be used by another accidentally. Note also that memcached has" NL |
| "### no authentication for reads or writes, so you must ensure that your" NL |
| "### memcached servers are only accessible by trusted users." NL |
| "" NL |
| "[" CONFIG_SECTION_CACHES "]" NL |
| "### When a cache-related error occurs, normally Subversion ignores it" NL |
| "### and continues, logging an error if the server is appropriately" NL |
| "### configured (and ignoring it with file:// access). To make" NL |
| "### Subversion never ignore cache errors, uncomment this line." NL |
| "# " CONFIG_OPTION_FAIL_STOP " = true" NL |
| "" NL |
| "[" CONFIG_SECTION_REP_SHARING "]" NL |
| "### To conserve space, the filesystem can optionally avoid storing" NL |
| "### duplicate representations. This comes at a slight cost in" NL |
| "### performance, as maintaining a database of shared representations can" NL |
| "### increase commit times. The space savings are dependent upon the size" NL |
| "### of the repository, the number of objects it contains and the amount of" NL |
| "### duplication between them, usually a function of the branching and" NL |
| "### merging process." NL |
| "###" NL |
| "### The following parameter enables rep-sharing in the repository. It can" NL |
| "### be switched on and off at will, but for best space-saving results" NL |
| "### should be enabled consistently over the life of the repository." NL |
| "### 'svnadmin verify' will check the rep-cache regardless of this setting." NL |
| "### rep-sharing is enabled by default." NL |
| "# " CONFIG_OPTION_ENABLE_REP_SHARING " = true" NL |
| "" NL |
| "[" CONFIG_SECTION_DELTIFICATION "]" NL |
| "### To conserve space, the filesystem stores data as differences against" NL |
| "### existing representations. This comes at a slight cost in performance," NL |
| "### as calculating differences can increase commit times. Reading data" NL |
| "### will also create higher CPU load and the data will be fragmented." NL |
| "### Since deltification tends to save significant amounts of disk space," NL |
| "### the overall I/O load can actually be lower." NL |
| "###" NL |
| "### The options in this section allow for tuning the deltification" NL |
| "### strategy. Their effects on data size and server performance may vary" NL |
| "### from one repository to another. Versions prior to 1.8 will ignore" NL |
| "### this section." NL |
| "###" NL |
| "### The following parameter enables deltification for directories. It can" NL |
| "### be switched on and off at will, but for best space-saving results" NL |
| "### should be enabled consistently over the life of the repository." NL |
| "### Repositories containing large directories will benefit greatly." NL |
| "### In rarely read repositories, the I/O overhead may be significant as" NL |
| "### cache hit rates will most likely be low" NL |
| "### directory deltification is disabled by default." NL |
| "# " CONFIG_OPTION_ENABLE_DIR_DELTIFICATION " = false" NL |
| "###" NL |
| "### The following parameter enables deltification for properties on files" NL |
| "### and directories. Overall, this is a minor tuning option but can save" NL |
| "### some disk space if you merge frequently or frequently change node" NL |
| "### properties. You should not activate this if rep-sharing has been" NL |
| "### disabled because this may result in a net increase in repository size." NL |
| "### property deltification is disabled by default." NL |
| "# " CONFIG_OPTION_ENABLE_PROPS_DELTIFICATION " = false" NL |
| "###" NL |
| "### During commit, the server may need to walk the whole change history of" NL |
| "### of a given node to find a suitable deltification base. This linear" NL |
| "### process can impact commit times, svnadmin load and similar operations." NL |
| "### This setting limits the depth of the deltification history. If the" NL |
| "### threshold has been reached, the node will be stored as fulltext and a" NL |
| "### new deltification history begins." NL |
| "### Note, this is unrelated to svn log." NL |
| "### Very large values rarely provide significant additional savings but" NL |
| "### can impact performance greatly - in particular if directory" NL |
| "### deltification has been activated. Very small values may be useful in" NL |
| "### repositories that are dominated by large, changing binaries." NL |
| "### Should be a power of two minus 1. A value of 0 will effectively" NL |
| "### disable deltification." NL |
| "### For 1.8, the default value is 1023; earlier versions have no limit." NL |
| "# " CONFIG_OPTION_MAX_DELTIFICATION_WALK " = 1023" NL |
| "###" NL |
| "### The skip-delta scheme used by FSFS tends to repeatably store redundant" NL |
| "### delta information where a simple delta against the latest version is" NL |
| "### often smaller. By default, 1.8+ will therefore use skip deltas only" NL |
| "### after the linear chain of deltas has grown beyond the threshold" NL |
| "### specified by this setting." NL |
| "### Values up to 64 can result in some reduction in repository size for" NL |
| "### the cost of quickly increasing I/O and CPU costs. Similarly, smaller" NL |
| "### numbers can reduce those costs at the cost of more disk space. For" NL |
| "### rarely read repositories or those containing larger binaries, this may" NL |
| "### present a better trade-off." NL |
| "### Should be a power of two. A value of 1 or smaller will cause the" NL |
| "### exclusive use of skip-deltas (as in pre-1.8)." NL |
| "### For 1.8, the default value is 16; earlier versions use 1." NL |
| "# " CONFIG_OPTION_MAX_LINEAR_DELTIFICATION " = 16" NL |
| "" NL |
| "[" CONFIG_SECTION_PACKED_REVPROPS "]" NL |
| "### This parameter controls the size (in kBytes) of packed revprop files." NL |
| "### Revprops of consecutive revisions will be concatenated into a single" NL |
| "### file up to but not exceeding the threshold given here. However, each" NL |
| "### pack file may be much smaller and revprops of a single revision may be" NL |
| "### much larger than the limit set here. The threshold will be applied" NL |
| "### before optional compression takes place." NL |
| "### Large values will reduce disk space usage at the expense of increased" NL |
| "### latency and CPU usage reading and changing individual revprops. They" NL |
| "### become an advantage when revprop caching has been enabled because a" NL |
| "### lot of data can be read in one go. Values smaller than 4 kByte will" NL |
| "### not improve latency any further and quickly render revprop packing" NL |
| "### ineffective." NL |
| "### revprop-pack-size is 64 kBytes by default for non-compressed revprop" NL |
| "### pack files and 256 kBytes when compression has been enabled." NL |
| "# " CONFIG_OPTION_REVPROP_PACK_SIZE " = 64" NL |
| "###" NL |
| "### To save disk space, packed revprop files may be compressed. Standard" NL |
| "### revprops tend to allow for very effective compression. Reading and" NL |
| "### even more so writing, become significantly more CPU intensive. With" NL |
| "### revprop caching enabled, the overhead can be offset by reduced I/O" NL |
| "### unless you often modify revprops after packing." NL |
| "### Compressing packed revprops is disabled by default." NL |
| "# " CONFIG_OPTION_COMPRESS_PACKED_REVPROPS " = false" NL |
| ; |
| #undef NL |
| return svn_io_file_create(svn_dirent_join(fs->path, PATH_CONFIG, pool), |
| fsfs_conf_contents, pool); |
| } |
| |
| static svn_error_t * |
| read_min_unpacked_rev(svn_revnum_t *min_unpacked_rev, |
| const char *path, |
| apr_pool_t *pool) |
| { |
| char buf[80]; |
| apr_file_t *file; |
| apr_size_t len; |
| |
| SVN_ERR(svn_io_file_open(&file, path, APR_READ | APR_BUFFERED, |
| APR_OS_DEFAULT, pool)); |
| len = sizeof(buf); |
| SVN_ERR(svn_io_read_length_line(file, buf, &len, pool)); |
| SVN_ERR(svn_io_file_close(file, pool)); |
| |
| *min_unpacked_rev = SVN_STR_TO_REV(buf); |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| update_min_unpacked_rev(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| SVN_ERR_ASSERT(ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT); |
| |
| return read_min_unpacked_rev(&ffd->min_unpacked_rev, |
| path_min_unpacked_rev(fs, pool), |
| pool); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__open(svn_fs_t *fs, const char *path, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| apr_file_t *uuid_file; |
| int format, max_files_per_dir; |
| char buf[APR_UUID_FORMATTED_LENGTH + 2]; |
| apr_size_t limit; |
| |
| fs->path = apr_pstrdup(fs->pool, path); |
| |
| /* Read the FS format number. */ |
| SVN_ERR(read_format(&format, &max_files_per_dir, |
| path_format(fs, pool), pool)); |
| |
| /* Now we've got a format number no matter what. */ |
| ffd->format = format; |
| ffd->max_files_per_dir = max_files_per_dir; |
| |
| /* Read in and cache the repository uuid. */ |
| SVN_ERR(svn_io_file_open(&uuid_file, path_uuid(fs, pool), |
| APR_READ | APR_BUFFERED, APR_OS_DEFAULT, pool)); |
| |
| limit = sizeof(buf); |
| SVN_ERR(svn_io_read_length_line(uuid_file, buf, &limit, pool)); |
| fs->uuid = apr_pstrdup(fs->pool, buf); |
| |
| SVN_ERR(svn_io_file_close(uuid_file, pool)); |
| |
| /* Read the min unpacked revision. */ |
| if (ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) |
| SVN_ERR(update_min_unpacked_rev(fs, pool)); |
| |
| /* Read the configuration file. */ |
| SVN_ERR(read_config(ffd, fs->path, pool)); |
| |
| return get_youngest(&(ffd->youngest_rev_cache), path, pool); |
| } |
| |
| /* Wrapper around svn_io_file_create which ignores EEXIST. */ |
| static svn_error_t * |
| create_file_ignore_eexist(const char *file, |
| const char *contents, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err = svn_io_file_create(file, contents, pool); |
| if (err && APR_STATUS_IS_EEXIST(err->apr_err)) |
| { |
| svn_error_clear(err); |
| err = SVN_NO_ERROR; |
| } |
| return svn_error_trace(err); |
| } |
| |
| /* forward declarations */ |
| |
| static svn_error_t * |
| pack_revprops_shard(const char *pack_file_dir, |
| const char *shard_path, |
| apr_int64_t shard, |
| int max_files_per_dir, |
| apr_off_t max_pack_size, |
| int compression_level, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *scratch_pool); |
| |
| static svn_error_t * |
| delete_revprops_shard(const char *shard_path, |
| apr_int64_t shard, |
| int max_files_per_dir, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *scratch_pool); |
| |
| /* In the filesystem FS, pack all revprop shards up to min_unpacked_rev. |
| * |
| * NOTE: Keep the old non-packed shards around until after the format bump. |
| * Otherwise, re-running upgrade will drop the packed revprop shard but |
| * have no unpacked data anymore. Call upgrade_cleanup_pack_revprops after |
| * the bump. |
| * |
| * Use SCRATCH_POOL for temporary allocations. |
| */ |
| static svn_error_t * |
| upgrade_pack_revprops(svn_fs_t *fs, |
| apr_pool_t *scratch_pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| const char *revprops_shard_path; |
| const char *revprops_pack_file_dir; |
| apr_int64_t shard; |
| apr_int64_t first_unpacked_shard |
| = ffd->min_unpacked_rev / ffd->max_files_per_dir; |
| |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| const char *revsprops_dir = svn_dirent_join(fs->path, PATH_REVPROPS_DIR, |
| scratch_pool); |
| int compression_level = ffd->compress_packed_revprops |
| ? SVN_DELTA_COMPRESSION_LEVEL_DEFAULT |
| : SVN_DELTA_COMPRESSION_LEVEL_NONE; |
| |
| /* first, pack all revprops shards to match the packed revision shards */ |
| for (shard = 0; shard < first_unpacked_shard; ++shard) |
| { |
| revprops_pack_file_dir = svn_dirent_join(revsprops_dir, |
| apr_psprintf(iterpool, |
| "%" APR_INT64_T_FMT PATH_EXT_PACKED_SHARD, |
| shard), |
| iterpool); |
| revprops_shard_path = svn_dirent_join(revsprops_dir, |
| apr_psprintf(iterpool, "%" APR_INT64_T_FMT, shard), |
| iterpool); |
| |
| SVN_ERR(pack_revprops_shard(revprops_pack_file_dir, revprops_shard_path, |
| shard, ffd->max_files_per_dir, |
| (int)(0.9 * ffd->revprop_pack_size), |
| compression_level, |
| NULL, NULL, iterpool)); |
| svn_pool_clear(iterpool); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* In the filesystem FS, remove all non-packed revprop shards up to |
| * min_unpacked_rev. Use SCRATCH_POOL for temporary allocations. |
| * See upgrade_pack_revprops for more info. |
| */ |
| static svn_error_t * |
| upgrade_cleanup_pack_revprops(svn_fs_t *fs, |
| apr_pool_t *scratch_pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| const char *revprops_shard_path; |
| apr_int64_t shard; |
| apr_int64_t first_unpacked_shard |
| = ffd->min_unpacked_rev / ffd->max_files_per_dir; |
| |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| const char *revsprops_dir = svn_dirent_join(fs->path, PATH_REVPROPS_DIR, |
| scratch_pool); |
| |
| /* delete the non-packed revprops shards afterwards */ |
| for (shard = 0; shard < first_unpacked_shard; ++shard) |
| { |
| revprops_shard_path = svn_dirent_join(revsprops_dir, |
| apr_psprintf(iterpool, "%" APR_INT64_T_FMT, shard), |
| iterpool); |
| SVN_ERR(delete_revprops_shard(revprops_shard_path, |
| shard, ffd->max_files_per_dir, |
| NULL, NULL, iterpool)); |
| svn_pool_clear(iterpool); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| upgrade_body(void *baton, apr_pool_t *pool) |
| { |
| svn_fs_t *fs = baton; |
| int format, max_files_per_dir; |
| const char *format_path = path_format(fs, pool); |
| svn_node_kind_t kind; |
| svn_boolean_t needs_revprop_shard_cleanup = FALSE; |
| |
| /* Read the FS format number and max-files-per-dir setting. */ |
| SVN_ERR(read_format(&format, &max_files_per_dir, format_path, pool)); |
| |
| /* If the config file does not exist, create one. */ |
| SVN_ERR(svn_io_check_path(svn_dirent_join(fs->path, PATH_CONFIG, pool), |
| &kind, pool)); |
| switch (kind) |
| { |
| case svn_node_none: |
| SVN_ERR(write_config(fs, pool)); |
| break; |
| case svn_node_file: |
| break; |
| default: |
| return svn_error_createf(SVN_ERR_FS_GENERAL, NULL, |
| _("'%s' is not a regular file." |
| " Please move it out of " |
| "the way and try again"), |
| svn_dirent_join(fs->path, PATH_CONFIG, pool)); |
| } |
| |
| /* If we're already up-to-date, there's nothing else to be done here. */ |
| if (format == SVN_FS_FS__FORMAT_NUMBER) |
| return SVN_NO_ERROR; |
| |
| /* If our filesystem predates the existance of the 'txn-current |
| file', make that file and its corresponding lock file. */ |
| if (format < SVN_FS_FS__MIN_TXN_CURRENT_FORMAT) |
| { |
| SVN_ERR(create_file_ignore_eexist(path_txn_current(fs, pool), "0\n", |
| pool)); |
| SVN_ERR(create_file_ignore_eexist(path_txn_current_lock(fs, pool), "", |
| pool)); |
| } |
| |
| /* If our filesystem predates the existance of the 'txn-protorevs' |
| dir, make that directory. */ |
| if (format < SVN_FS_FS__MIN_PROTOREVS_DIR_FORMAT) |
| { |
| /* We don't use path_txn_proto_rev() here because it expects |
| we've already bumped our format. */ |
| SVN_ERR(svn_io_make_dir_recursively( |
| svn_dirent_join(fs->path, PATH_TXN_PROTOS_DIR, pool), pool)); |
| } |
| |
| /* If our filesystem is new enough, write the min unpacked rev file. */ |
| if (format < SVN_FS_FS__MIN_PACKED_FORMAT) |
| SVN_ERR(svn_io_file_create(path_min_unpacked_rev(fs, pool), "0\n", pool)); |
| |
| /* If the file system supports revision packing but not revprop packing |
| *and* the FS has been sharded, pack the revprops up to the point that |
| revision data has been packed. However, keep the non-packed revprop |
| files around until after the format bump */ |
| if ( format >= SVN_FS_FS__MIN_PACKED_FORMAT |
| && format < SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT |
| && max_files_per_dir > 0) |
| { |
| needs_revprop_shard_cleanup = TRUE; |
| SVN_ERR(upgrade_pack_revprops(fs, pool)); |
| } |
| |
| /* Bump the format file. */ |
| SVN_ERR(write_format(format_path, SVN_FS_FS__FORMAT_NUMBER, |
| max_files_per_dir, TRUE, pool)); |
| |
| /* Now, it is safe to remove the redundant revprop files. */ |
| if (needs_revprop_shard_cleanup) |
| SVN_ERR(upgrade_cleanup_pack_revprops(fs, pool)); |
| |
| /* Done */ |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_fs_fs__upgrade(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| return svn_fs_fs__with_write_lock(fs, upgrade_body, (void *)fs, pool); |
| } |
| |
| |
| /* Functions for dealing with recoverable errors on mutable files |
| * |
| * Revprops, current, and txn-current files are mutable; that is, they |
| * change as part of normal fsfs operation, in constrat to revs files, or |
| * the format file, which are written once at create (or upgrade) time. |
| * When more than one host writes to the same repository, we will |
| * sometimes see these recoverable errors when accesssing these files. |
| * |
| * These errors all relate to NFS, and thus we only use this retry code if |
| * ESTALE is defined. |
| * |
| ** ESTALE |
| * |
| * In NFS v3 and under, the server doesn't track opened files. If you |
| * unlink(2) or rename(2) a file held open by another process *on the |
| * same host*, that host's kernel typically renames the file to |
| * .nfsXXXX and automatically deletes that when it's no longer open, |
| * but this behavior is not required. |
| * |
| * For obvious reasons, this does not work *across hosts*. No one |
| * knows about the opened file; not the server, and not the deleting |
| * client. So the file vanishes, and the reader gets stale NFS file |
| * handle. |
| * |
| ** EIO, ENOENT |
| * |
| * Some client implementations (at least the 2.6.18.5 kernel that ships |
| * with Ubuntu Dapper) sometimes give spurious ENOENT (only on open) or |
| * even EIO errors when trying to read these files that have been renamed |
| * over on some other host. |
| * |
| ** Solution |
| * |
| * Try open and read of such files in try_stringbuf_from_file(). Call |
| * this function within a loop of RECOVERABLE_RETRY_COUNT iterations |
| * (though, realistically, the second try will succeed). |
| */ |
| |
| #define RECOVERABLE_RETRY_COUNT 10 |
| |
| /* Read the file at PATH and return its content in *CONTENT. *CONTENT will |
| * not be modified unless the whole file was read successfully. |
| * |
| * ESTALE, EIO and ENOENT will not cause this function to return an error |
| * unless LAST_ATTEMPT has been set. If MISSING is not NULL, indicate |
| * missing files (ENOENT) there. |
| * |
| * Use POOL for allocations. |
| */ |
| static svn_error_t * |
| try_stringbuf_from_file(svn_stringbuf_t **content, |
| svn_boolean_t *missing, |
| const char *path, |
| svn_boolean_t last_attempt, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err = svn_stringbuf_from_file2(content, path, pool); |
| if (missing) |
| *missing = FALSE; |
| |
| if (err) |
| { |
| *content = NULL; |
| |
| if (APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| if (!last_attempt) |
| { |
| svn_error_clear(err); |
| if (missing) |
| *missing = TRUE; |
| return SVN_NO_ERROR; |
| } |
| } |
| #ifdef ESTALE |
| else if (APR_TO_OS_ERROR(err->apr_err) == ESTALE |
| || APR_TO_OS_ERROR(err->apr_err) == EIO) |
| { |
| if (!last_attempt) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| } |
| #endif |
| } |
| |
| return svn_error_trace(err); |
| } |
| |
| /* Read the 'current' file FNAME and store the contents in *BUF. |
| Allocations are performed in POOL. */ |
| static svn_error_t * |
| read_content(svn_stringbuf_t **content, const char *fname, apr_pool_t *pool) |
| { |
| int i; |
| *content = NULL; |
| |
| for (i = 0; !*content && (i < RECOVERABLE_RETRY_COUNT); ++i) |
| SVN_ERR(try_stringbuf_from_file(content, NULL, |
| fname, i + 1 < RECOVERABLE_RETRY_COUNT, |
| pool)); |
| |
| if (!*content) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Can't read '%s'"), |
| svn_dirent_local_style(fname, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Find the youngest revision in a repository at path FS_PATH and |
| return it in *YOUNGEST_P. Perform temporary allocations in |
| POOL. */ |
| static svn_error_t * |
| get_youngest(svn_revnum_t *youngest_p, |
| const char *fs_path, |
| apr_pool_t *pool) |
| { |
| svn_stringbuf_t *buf; |
| SVN_ERR(read_content(&buf, svn_dirent_join(fs_path, PATH_CURRENT, pool), |
| pool)); |
| |
| *youngest_p = SVN_STR_TO_REV(buf->data); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_fs_fs__youngest_rev(svn_revnum_t *youngest_p, |
| svn_fs_t *fs, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| SVN_ERR(get_youngest(youngest_p, fs->path, pool)); |
| ffd->youngest_rev_cache = *youngest_p; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Given a revision file FILE that has been pre-positioned at the |
| beginning of a Node-Rev header block, read in that header block and |
| store it in the apr_hash_t HEADERS. All allocations will be from |
| POOL. */ |
| static svn_error_t * read_header_block(apr_hash_t **headers, |
| svn_stream_t *stream, |
| apr_pool_t *pool) |
| { |
| *headers = apr_hash_make(pool); |
| |
| while (1) |
| { |
| svn_stringbuf_t *header_str; |
| const char *name, *value; |
| apr_size_t i = 0; |
| svn_boolean_t eof; |
| |
| SVN_ERR(svn_stream_readline(stream, &header_str, "\n", &eof, pool)); |
| |
| if (eof || header_str->len == 0) |
| break; /* end of header block */ |
| |
| while (header_str->data[i] != ':') |
| { |
| if (header_str->data[i] == '\0') |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Found malformed header '%s' in " |
| "revision file"), |
| header_str->data); |
| i++; |
| } |
| |
| /* Create a 'name' string and point to it. */ |
| header_str->data[i] = '\0'; |
| name = header_str->data; |
| |
| /* Skip over the NULL byte and the space following it. */ |
| i += 2; |
| |
| if (i > header_str->len) |
| { |
| /* Restore the original line for the error. */ |
| i -= 2; |
| header_str->data[i] = ':'; |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Found malformed header '%s' in " |
| "revision file"), |
| header_str->data); |
| } |
| |
| value = header_str->data + i; |
| |
| /* header_str is safely in our pool, so we can use bits of it as |
| key and value. */ |
| svn_hash_sets(*headers, name, value); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return SVN_ERR_FS_NO_SUCH_REVISION if the given revision is newer |
| than the current youngest revision or is simply not a valid |
| revision number, else return success. |
| |
| FSFS is based around the concept that commits only take effect when |
| the number in "current" is bumped. Thus if there happens to be a rev |
| or revprops file installed for a revision higher than the one recorded |
| in "current" (because a commit failed between installing the rev file |
| and bumping "current", or because an administrator rolled back the |
| repository by resetting "current" without deleting rev files, etc), it |
| ought to be completely ignored. This function provides the check |
| by which callers can make that decision. */ |
| static svn_error_t * |
| ensure_revision_exists(svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| if (! SVN_IS_VALID_REVNUM(rev)) |
| return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| _("Invalid revision number '%ld'"), rev); |
| |
| |
| /* Did the revision exist the last time we checked the current |
| file? */ |
| if (rev <= ffd->youngest_rev_cache) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(get_youngest(&(ffd->youngest_rev_cache), fs->path, pool)); |
| |
| /* Check again. */ |
| if (rev <= ffd->youngest_rev_cache) |
| return SVN_NO_ERROR; |
| |
| return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| _("No such revision %ld"), rev); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__revision_exists(svn_revnum_t rev, |
| svn_fs_t *fs, |
| apr_pool_t *pool) |
| { |
| /* Different order of parameters. */ |
| SVN_ERR(ensure_revision_exists(fs, rev, pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Open the correct revision file for REV. If the filesystem FS has |
| been packed, *FILE will be set to the packed file; otherwise, set *FILE |
| to the revision file for REV. Return SVN_ERR_FS_NO_SUCH_REVISION if the |
| file doesn't exist. |
| |
| TODO: Consider returning an indication of whether this is a packed rev |
| file, so the caller need not rely on is_packed_rev() which in turn |
| relies on the cached FFD->min_unpacked_rev value not having changed |
| since the rev file was opened. |
| |
| Use POOL for allocations. */ |
| static svn_error_t * |
| open_pack_or_rev_file(apr_file_t **file, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_error_t *err; |
| const char *path; |
| svn_boolean_t retry = FALSE; |
| |
| do |
| { |
| err = svn_fs_fs__path_rev_absolute(&path, fs, rev, pool); |
| |
| /* open the revision file in buffered r/o mode */ |
| if (! err) |
| err = svn_io_file_open(file, path, |
| APR_READ | APR_BUFFERED, APR_OS_DEFAULT, pool); |
| |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| if (ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) |
| { |
| /* Could not open the file. This may happen if the |
| * file once existed but got packed later. */ |
| svn_error_clear(err); |
| |
| /* if that was our 2nd attempt, leave it at that. */ |
| if (retry) |
| return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| _("No such revision %ld"), rev); |
| |
| /* We failed for the first time. Refresh cache & retry. */ |
| SVN_ERR(update_min_unpacked_rev(fs, pool)); |
| |
| retry = TRUE; |
| } |
| else |
| { |
| svn_error_clear(err); |
| return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| _("No such revision %ld"), rev); |
| } |
| } |
| else |
| { |
| retry = FALSE; |
| } |
| } |
| while (retry); |
| |
| return svn_error_trace(err); |
| } |
| |
| /* Reads a line from STREAM and converts it to a 64 bit integer to be |
| * returned in *RESULT. If we encounter eof, set *HIT_EOF and leave |
| * *RESULT unchanged. If HIT_EOF is NULL, EOF causes an "corrupt FS" |
| * error return. |
| * SCRATCH_POOL is used for temporary allocations. |
| */ |
| static svn_error_t * |
| read_number_from_stream(apr_int64_t *result, |
| svn_boolean_t *hit_eof, |
| svn_stream_t *stream, |
| apr_pool_t *scratch_pool) |
| { |
| svn_stringbuf_t *sb; |
| svn_boolean_t eof; |
| svn_error_t *err; |
| |
| SVN_ERR(svn_stream_readline(stream, &sb, "\n", &eof, scratch_pool)); |
| if (hit_eof) |
| *hit_eof = eof; |
| else |
| if (eof) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, _("Unexpected EOF")); |
| |
| if (!eof) |
| { |
| err = svn_cstring_atoi64(result, sb->data); |
| if (err) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, err, |
| _("Number '%s' invalid or too large"), |
| sb->data); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Given REV in FS, set *REV_OFFSET to REV's offset in the packed file. |
| Use POOL for temporary allocations. */ |
| static svn_error_t * |
| get_packed_offset(apr_off_t *rev_offset, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_stream_t *manifest_stream; |
| svn_boolean_t is_cached; |
| svn_revnum_t shard; |
| apr_int64_t shard_pos; |
| apr_array_header_t *manifest; |
| apr_pool_t *iterpool; |
| |
| shard = rev / ffd->max_files_per_dir; |
| |
| /* position of the shard within the manifest */ |
| shard_pos = rev % ffd->max_files_per_dir; |
| |
| /* fetch exactly that element into *rev_offset, if the manifest is found |
| in the cache */ |
| SVN_ERR(svn_cache__get_partial((void **) rev_offset, &is_cached, |
| ffd->packed_offset_cache, &shard, |
| svn_fs_fs__get_sharded_offset, &shard_pos, |
| pool)); |
| |
| if (is_cached) |
| return SVN_NO_ERROR; |
| |
| /* Open the manifest file. */ |
| SVN_ERR(svn_stream_open_readonly(&manifest_stream, |
| path_rev_packed(fs, rev, PATH_MANIFEST, |
| pool), |
| pool, pool)); |
| |
| /* While we're here, let's just read the entire manifest file into an array, |
| so we can cache the entire thing. */ |
| iterpool = svn_pool_create(pool); |
| manifest = apr_array_make(pool, ffd->max_files_per_dir, sizeof(apr_off_t)); |
| while (1) |
| { |
| svn_boolean_t eof; |
| apr_int64_t val; |
| |
| svn_pool_clear(iterpool); |
| SVN_ERR(read_number_from_stream(&val, &eof, manifest_stream, iterpool)); |
| if (eof) |
| break; |
| |
| APR_ARRAY_PUSH(manifest, apr_off_t) = (apr_off_t)val; |
| } |
| svn_pool_destroy(iterpool); |
| |
| *rev_offset = APR_ARRAY_IDX(manifest, rev % ffd->max_files_per_dir, |
| apr_off_t); |
| |
| /* Close up shop and cache the array. */ |
| SVN_ERR(svn_stream_close(manifest_stream)); |
| return svn_cache__set(ffd->packed_offset_cache, &shard, manifest, pool); |
| } |
| |
| /* Open the revision file for revision REV in filesystem FS and store |
| the newly opened file in FILE. Seek to location OFFSET before |
| returning. Perform temporary allocations in POOL. */ |
| static svn_error_t * |
| open_and_seek_revision(apr_file_t **file, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_off_t offset, |
| apr_pool_t *pool) |
| { |
| apr_file_t *rev_file; |
| |
| SVN_ERR(ensure_revision_exists(fs, rev, pool)); |
| |
| SVN_ERR(open_pack_or_rev_file(&rev_file, fs, rev, pool)); |
| |
| if (is_packed_rev(fs, rev)) |
| { |
| apr_off_t rev_offset; |
| |
| SVN_ERR(get_packed_offset(&rev_offset, fs, rev, pool)); |
| offset += rev_offset; |
| } |
| |
| SVN_ERR(svn_io_file_seek(rev_file, APR_SET, &offset, pool)); |
| |
| *file = rev_file; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Open the representation for a node-revision in transaction TXN_ID |
| in filesystem FS and store the newly opened file in FILE. Seek to |
| location OFFSET before returning. Perform temporary allocations in |
| POOL. Only appropriate for file contents, nor props or directory |
| contents. */ |
| static svn_error_t * |
| open_and_seek_transaction(apr_file_t **file, |
| svn_fs_t *fs, |
| const char *txn_id, |
| representation_t *rep, |
| apr_pool_t *pool) |
| { |
| apr_file_t *rev_file; |
| apr_off_t offset; |
| |
| SVN_ERR(svn_io_file_open(&rev_file, path_txn_proto_rev(fs, txn_id, pool), |
| APR_READ | APR_BUFFERED, APR_OS_DEFAULT, pool)); |
| |
| offset = rep->offset; |
| SVN_ERR(svn_io_file_seek(rev_file, APR_SET, &offset, pool)); |
| |
| *file = rev_file; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Given a node-id ID, and a representation REP in filesystem FS, open |
| the correct file and seek to the correction location. Store this |
| file in *FILE_P. Perform any allocations in POOL. */ |
| static svn_error_t * |
| open_and_seek_representation(apr_file_t **file_p, |
| svn_fs_t *fs, |
| representation_t *rep, |
| apr_pool_t *pool) |
| { |
| if (! rep->txn_id) |
| return open_and_seek_revision(file_p, fs, rep->revision, rep->offset, |
| pool); |
| else |
| return open_and_seek_transaction(file_p, fs, rep->txn_id, rep, pool); |
| } |
| |
| /* Parse the description of a representation from STRING and store it |
| into *REP_P. If the representation is mutable (the revision is |
| given as -1), then use TXN_ID for the representation's txn_id |
| field. If MUTABLE_REP_TRUNCATED is true, then this representation |
| is for property or directory contents, and no information will be |
| expected except the "-1" revision number for a mutable |
| representation. Allocate *REP_P in POOL. */ |
| static svn_error_t * |
| read_rep_offsets_body(representation_t **rep_p, |
| char *string, |
| const char *txn_id, |
| svn_boolean_t mutable_rep_truncated, |
| apr_pool_t *pool) |
| { |
| representation_t *rep; |
| char *str; |
| apr_int64_t val; |
| |
| rep = apr_pcalloc(pool, sizeof(*rep)); |
| *rep_p = rep; |
| |
| str = svn_cstring_tokenize(" ", &string); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed text representation offset line in node-rev")); |
| |
| |
| rep->revision = SVN_STR_TO_REV(str); |
| if (rep->revision == SVN_INVALID_REVNUM) |
| { |
| rep->txn_id = txn_id; |
| if (mutable_rep_truncated) |
| return SVN_NO_ERROR; |
| } |
| |
| str = svn_cstring_tokenize(" ", &string); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed text representation offset line in node-rev")); |
| |
| SVN_ERR(svn_cstring_atoi64(&val, str)); |
| rep->offset = (apr_off_t)val; |
| |
| str = svn_cstring_tokenize(" ", &string); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed text representation offset line in node-rev")); |
| |
| SVN_ERR(svn_cstring_atoi64(&val, str)); |
| rep->size = (svn_filesize_t)val; |
| |
| str = svn_cstring_tokenize(" ", &string); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed text representation offset line in node-rev")); |
| |
| SVN_ERR(svn_cstring_atoi64(&val, str)); |
| rep->expanded_size = (svn_filesize_t)val; |
| |
| /* Read in the MD5 hash. */ |
| str = svn_cstring_tokenize(" ", &string); |
| if ((str == NULL) || (strlen(str) != (APR_MD5_DIGESTSIZE * 2))) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed text representation offset line in node-rev")); |
| |
| SVN_ERR(svn_checksum_parse_hex(&rep->md5_checksum, svn_checksum_md5, str, |
| pool)); |
| |
| /* The remaining fields are only used for formats >= 4, so check that. */ |
| str = svn_cstring_tokenize(" ", &string); |
| if (str == NULL) |
| return SVN_NO_ERROR; |
| |
| /* Read the SHA1 hash. */ |
| if (strlen(str) != (APR_SHA1_DIGESTSIZE * 2)) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed text representation offset line in node-rev")); |
| |
| SVN_ERR(svn_checksum_parse_hex(&rep->sha1_checksum, svn_checksum_sha1, str, |
| pool)); |
| |
| /* Read the uniquifier. */ |
| str = svn_cstring_tokenize(" ", &string); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed text representation offset line in node-rev")); |
| |
| rep->uniquifier = apr_pstrdup(pool, str); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Wrap read_rep_offsets_body(), extracting its TXN_ID from our NODEREV_ID, |
| and adding an error message. */ |
| static svn_error_t * |
| read_rep_offsets(representation_t **rep_p, |
| char *string, |
| const svn_fs_id_t *noderev_id, |
| svn_boolean_t mutable_rep_truncated, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| const char *txn_id; |
| |
| if (noderev_id) |
| txn_id = svn_fs_fs__id_txn_id(noderev_id); |
| else |
| txn_id = NULL; |
| |
| err = read_rep_offsets_body(rep_p, string, txn_id, mutable_rep_truncated, |
| pool); |
| if (err) |
| { |
| const svn_string_t *id_unparsed = svn_fs_fs__id_unparse(noderev_id, pool); |
| const char *where; |
| where = apr_psprintf(pool, |
| _("While reading representation offsets " |
| "for node-revision '%s':"), |
| noderev_id ? id_unparsed->data : "(null)"); |
| |
| return svn_error_quick_wrap(err, where); |
| } |
| else |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| err_dangling_id(svn_fs_t *fs, const svn_fs_id_t *id) |
| { |
| svn_string_t *id_str = svn_fs_fs__id_unparse(id, fs->pool); |
| return svn_error_createf |
| (SVN_ERR_FS_ID_NOT_FOUND, 0, |
| _("Reference to non-existent node '%s' in filesystem '%s'"), |
| id_str->data, fs->path); |
| } |
| |
| /* Look up the NODEREV_P for ID in FS' node revsion cache. If noderev |
| * caching has been enabled and the data can be found, IS_CACHED will |
| * be set to TRUE. The noderev will be allocated from POOL. |
| * |
| * Non-permanent ids (e.g. ids within a TXN) will not be cached. |
| */ |
| static svn_error_t * |
| get_cached_node_revision_body(node_revision_t **noderev_p, |
| svn_fs_t *fs, |
| const svn_fs_id_t *id, |
| svn_boolean_t *is_cached, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| if (! ffd->node_revision_cache || svn_fs_fs__id_txn_id(id)) |
| { |
| *is_cached = FALSE; |
| } |
| else |
| { |
| pair_cache_key_t key = { 0 }; |
| |
| key.revision = svn_fs_fs__id_rev(id); |
| key.second = svn_fs_fs__id_offset(id); |
| SVN_ERR(svn_cache__get((void **) noderev_p, |
| is_cached, |
| ffd->node_revision_cache, |
| &key, |
| pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* If noderev caching has been enabled, store the NODEREV_P for the given ID |
| * in FS' node revsion cache. SCRATCH_POOL is used for temporary allcations. |
| * |
| * Non-permanent ids (e.g. ids within a TXN) will not be cached. |
| */ |
| static svn_error_t * |
| set_cached_node_revision_body(node_revision_t *noderev_p, |
| svn_fs_t *fs, |
| const svn_fs_id_t *id, |
| apr_pool_t *scratch_pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| if (ffd->node_revision_cache && !svn_fs_fs__id_txn_id(id)) |
| { |
| pair_cache_key_t key = { 0 }; |
| |
| key.revision = svn_fs_fs__id_rev(id); |
| key.second = svn_fs_fs__id_offset(id); |
| return svn_cache__set(ffd->node_revision_cache, |
| &key, |
| noderev_p, |
| scratch_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Get the node-revision for the node ID in FS. |
| Set *NODEREV_P to the new node-revision structure, allocated in POOL. |
| See svn_fs_fs__get_node_revision, which wraps this and adds another |
| error. */ |
| static svn_error_t * |
| get_node_revision_body(node_revision_t **noderev_p, |
| svn_fs_t *fs, |
| const svn_fs_id_t *id, |
| apr_pool_t *pool) |
| { |
| apr_file_t *revision_file; |
| svn_error_t *err; |
| svn_boolean_t is_cached = FALSE; |
| |
| /* First, try a cache lookup. If that succeeds, we are done here. */ |
| SVN_ERR(get_cached_node_revision_body(noderev_p, fs, id, &is_cached, pool)); |
| if (is_cached) |
| return SVN_NO_ERROR; |
| |
| if (svn_fs_fs__id_txn_id(id)) |
| { |
| /* This is a transaction node-rev. */ |
| err = svn_io_file_open(&revision_file, path_txn_node_rev(fs, id, pool), |
| APR_READ | APR_BUFFERED, APR_OS_DEFAULT, pool); |
| } |
| else |
| { |
| /* This is a revision node-rev. */ |
| err = open_and_seek_revision(&revision_file, fs, |
| svn_fs_fs__id_rev(id), |
| svn_fs_fs__id_offset(id), |
| pool); |
| } |
| |
| if (err) |
| { |
| if (APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| svn_error_clear(err); |
| return svn_error_trace(err_dangling_id(fs, id)); |
| } |
| |
| return svn_error_trace(err); |
| } |
| |
| SVN_ERR(svn_fs_fs__read_noderev(noderev_p, |
| svn_stream_from_aprfile2(revision_file, FALSE, |
| pool), |
| pool)); |
| |
| /* The noderev is not in cache, yet. Add it, if caching has been enabled. */ |
| return set_cached_node_revision_body(*noderev_p, fs, id, pool); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__read_noderev(node_revision_t **noderev_p, |
| svn_stream_t *stream, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *headers; |
| node_revision_t *noderev; |
| char *value; |
| const char *noderev_id; |
| |
| SVN_ERR(read_header_block(&headers, stream, pool)); |
| |
| noderev = apr_pcalloc(pool, sizeof(*noderev)); |
| |
| /* Read the node-rev id. */ |
| value = svn_hash_gets(headers, HEADER_ID); |
| if (value == NULL) |
| /* ### More information: filename/offset coordinates */ |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Missing id field in node-rev")); |
| |
| SVN_ERR(svn_stream_close(stream)); |
| |
| noderev->id = svn_fs_fs__id_parse(value, strlen(value), pool); |
| noderev_id = value; /* for error messages later */ |
| |
| /* Read the type. */ |
| value = svn_hash_gets(headers, HEADER_TYPE); |
| |
| if ((value == NULL) || |
| (strcmp(value, KIND_FILE) != 0 && strcmp(value, KIND_DIR))) |
| /* ### s/kind/type/ */ |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Missing kind field in node-rev '%s'"), |
| noderev_id); |
| |
| noderev->kind = (strcmp(value, KIND_FILE) == 0) ? svn_node_file |
| : svn_node_dir; |
| |
| /* Read the 'count' field. */ |
| value = svn_hash_gets(headers, HEADER_COUNT); |
| if (value) |
| SVN_ERR(svn_cstring_atoi(&noderev->predecessor_count, value)); |
| else |
| noderev->predecessor_count = 0; |
| |
| /* Get the properties location. */ |
| value = svn_hash_gets(headers, HEADER_PROPS); |
| if (value) |
| { |
| SVN_ERR(read_rep_offsets(&noderev->prop_rep, value, |
| noderev->id, TRUE, pool)); |
| } |
| |
| /* Get the data location. */ |
| value = svn_hash_gets(headers, HEADER_TEXT); |
| if (value) |
| { |
| SVN_ERR(read_rep_offsets(&noderev->data_rep, value, |
| noderev->id, |
| (noderev->kind == svn_node_dir), pool)); |
| } |
| |
| /* Get the created path. */ |
| value = svn_hash_gets(headers, HEADER_CPATH); |
| if (value == NULL) |
| { |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Missing cpath field in node-rev '%s'"), |
| noderev_id); |
| } |
| else |
| { |
| noderev->created_path = apr_pstrdup(pool, value); |
| } |
| |
| /* Get the predecessor ID. */ |
| value = svn_hash_gets(headers, HEADER_PRED); |
| if (value) |
| noderev->predecessor_id = svn_fs_fs__id_parse(value, strlen(value), |
| pool); |
| |
| /* Get the copyroot. */ |
| value = svn_hash_gets(headers, HEADER_COPYROOT); |
| if (value == NULL) |
| { |
| noderev->copyroot_path = apr_pstrdup(pool, noderev->created_path); |
| noderev->copyroot_rev = svn_fs_fs__id_rev(noderev->id); |
| } |
| else |
| { |
| char *str; |
| |
| str = svn_cstring_tokenize(" ", &value); |
| if (str == NULL) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed copyroot line in node-rev '%s'"), |
| noderev_id); |
| |
| noderev->copyroot_rev = SVN_STR_TO_REV(str); |
| |
| if (*value == '\0') |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed copyroot line in node-rev '%s'"), |
| noderev_id); |
| noderev->copyroot_path = apr_pstrdup(pool, value); |
| } |
| |
| /* Get the copyfrom. */ |
| value = svn_hash_gets(headers, HEADER_COPYFROM); |
| if (value == NULL) |
| { |
| noderev->copyfrom_path = NULL; |
| noderev->copyfrom_rev = SVN_INVALID_REVNUM; |
| } |
| else |
| { |
| char *str = svn_cstring_tokenize(" ", &value); |
| if (str == NULL) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed copyfrom line in node-rev '%s'"), |
| noderev_id); |
| |
| noderev->copyfrom_rev = SVN_STR_TO_REV(str); |
| |
| if (*value == 0) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed copyfrom line in node-rev '%s'"), |
| noderev_id); |
| noderev->copyfrom_path = apr_pstrdup(pool, value); |
| } |
| |
| /* Get whether this is a fresh txn root. */ |
| value = svn_hash_gets(headers, HEADER_FRESHTXNRT); |
| noderev->is_fresh_txn_root = (value != NULL); |
| |
| /* Get the mergeinfo count. */ |
| value = svn_hash_gets(headers, HEADER_MINFO_CNT); |
| if (value) |
| SVN_ERR(svn_cstring_atoi64(&noderev->mergeinfo_count, value)); |
| else |
| noderev->mergeinfo_count = 0; |
| |
| /* Get whether *this* node has mergeinfo. */ |
| value = svn_hash_gets(headers, HEADER_MINFO_HERE); |
| noderev->has_mergeinfo = (value != NULL); |
| |
| *noderev_p = noderev; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__get_node_revision(node_revision_t **noderev_p, |
| svn_fs_t *fs, |
| const svn_fs_id_t *id, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err = get_node_revision_body(noderev_p, fs, id, pool); |
| if (err && err->apr_err == SVN_ERR_FS_CORRUPT) |
| { |
| svn_string_t *id_string = svn_fs_fs__id_unparse(id, pool); |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, err, |
| "Corrupt node-revision '%s'", |
| id_string->data); |
| } |
| return svn_error_trace(err); |
| } |
| |
| |
| /* Return a formatted string, compatible with filesystem format FORMAT, |
| that represents the location of representation REP. If |
| MUTABLE_REP_TRUNCATED is given, the rep is for props or dir contents, |
| and only a "-1" revision number will be given for a mutable rep. |
| If MAY_BE_CORRUPT is true, guard for NULL when constructing the string. |
| Perform the allocation from POOL. */ |
| static const char * |
| representation_string(representation_t *rep, |
| int format, |
| svn_boolean_t mutable_rep_truncated, |
| svn_boolean_t may_be_corrupt, |
| apr_pool_t *pool) |
| { |
| if (rep->txn_id && mutable_rep_truncated) |
| return "-1"; |
| |
| #define DISPLAY_MAYBE_NULL_CHECKSUM(checksum) \ |
| ((!may_be_corrupt || (checksum) != NULL) \ |
| ? svn_checksum_to_cstring_display((checksum), pool) \ |
| : "(null)") |
| |
| if (format < SVN_FS_FS__MIN_REP_SHARING_FORMAT || rep->sha1_checksum == NULL) |
| return apr_psprintf(pool, "%ld %" APR_OFF_T_FMT " %" SVN_FILESIZE_T_FMT |
| " %" SVN_FILESIZE_T_FMT " %s", |
| rep->revision, rep->offset, rep->size, |
| rep->expanded_size, |
| DISPLAY_MAYBE_NULL_CHECKSUM(rep->md5_checksum)); |
| |
| return apr_psprintf(pool, "%ld %" APR_OFF_T_FMT " %" SVN_FILESIZE_T_FMT |
| " %" SVN_FILESIZE_T_FMT " %s %s %s", |
| rep->revision, rep->offset, rep->size, |
| rep->expanded_size, |
| DISPLAY_MAYBE_NULL_CHECKSUM(rep->md5_checksum), |
| DISPLAY_MAYBE_NULL_CHECKSUM(rep->sha1_checksum), |
| rep->uniquifier); |
| |
| #undef DISPLAY_MAYBE_NULL_CHECKSUM |
| |
| } |
| |
| |
| svn_error_t * |
| svn_fs_fs__write_noderev(svn_stream_t *outfile, |
| node_revision_t *noderev, |
| int format, |
| svn_boolean_t include_mergeinfo, |
| apr_pool_t *pool) |
| { |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_ID ": %s\n", |
| svn_fs_fs__id_unparse(noderev->id, |
| pool)->data)); |
| |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_TYPE ": %s\n", |
| (noderev->kind == svn_node_file) ? |
| KIND_FILE : KIND_DIR)); |
| |
| if (noderev->predecessor_id) |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_PRED ": %s\n", |
| svn_fs_fs__id_unparse(noderev->predecessor_id, |
| pool)->data)); |
| |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_COUNT ": %d\n", |
| noderev->predecessor_count)); |
| |
| if (noderev->data_rep) |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_TEXT ": %s\n", |
| representation_string(noderev->data_rep, |
| format, |
| (noderev->kind |
| == svn_node_dir), |
| FALSE, |
| pool))); |
| |
| if (noderev->prop_rep) |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_PROPS ": %s\n", |
| representation_string(noderev->prop_rep, format, |
| TRUE, FALSE, pool))); |
| |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_CPATH ": %s\n", |
| noderev->created_path)); |
| |
| if (noderev->copyfrom_path) |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_COPYFROM ": %ld" |
| " %s\n", |
| noderev->copyfrom_rev, |
| noderev->copyfrom_path)); |
| |
| if ((noderev->copyroot_rev != svn_fs_fs__id_rev(noderev->id)) || |
| (strcmp(noderev->copyroot_path, noderev->created_path) != 0)) |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_COPYROOT ": %ld" |
| " %s\n", |
| noderev->copyroot_rev, |
| noderev->copyroot_path)); |
| |
| if (noderev->is_fresh_txn_root) |
| SVN_ERR(svn_stream_puts(outfile, HEADER_FRESHTXNRT ": y\n")); |
| |
| if (include_mergeinfo) |
| { |
| if (noderev->mergeinfo_count > 0) |
| SVN_ERR(svn_stream_printf(outfile, pool, HEADER_MINFO_CNT ": %" |
| APR_INT64_T_FMT "\n", |
| noderev->mergeinfo_count)); |
| |
| if (noderev->has_mergeinfo) |
| SVN_ERR(svn_stream_puts(outfile, HEADER_MINFO_HERE ": y\n")); |
| } |
| |
| return svn_stream_puts(outfile, "\n"); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__put_node_revision(svn_fs_t *fs, |
| const svn_fs_id_t *id, |
| node_revision_t *noderev, |
| svn_boolean_t fresh_txn_root, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| apr_file_t *noderev_file; |
| const char *txn_id = svn_fs_fs__id_txn_id(id); |
| |
| noderev->is_fresh_txn_root = fresh_txn_root; |
| |
| if (! txn_id) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Attempted to write to non-transaction '%s'"), |
| svn_fs_fs__id_unparse(id, pool)->data); |
| |
| SVN_ERR(svn_io_file_open(&noderev_file, path_txn_node_rev(fs, id, pool), |
| APR_WRITE | APR_CREATE | APR_TRUNCATE |
| | APR_BUFFERED, APR_OS_DEFAULT, pool)); |
| |
| SVN_ERR(svn_fs_fs__write_noderev(svn_stream_from_aprfile2(noderev_file, TRUE, |
| pool), |
| noderev, ffd->format, |
| svn_fs_fs__fs_supports_mergeinfo(fs), |
| pool)); |
| |
| SVN_ERR(svn_io_file_close(noderev_file, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* For the in-transaction NODEREV within FS, write the sha1->rep mapping |
| * file in the respective transaction, if rep sharing has been enabled etc. |
| * Use POOL for temporary allocations. |
| */ |
| static svn_error_t * |
| store_sha1_rep_mapping(svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* if rep sharing has been enabled and the noderev has a data rep and |
| * its SHA-1 is known, store the rep struct under its SHA1. */ |
| if ( ffd->rep_sharing_allowed |
| && noderev->data_rep |
| && noderev->data_rep->sha1_checksum) |
| { |
| apr_file_t *rep_file; |
| const char *file_name = path_txn_sha1(fs, |
| svn_fs_fs__id_txn_id(noderev->id), |
| noderev->data_rep->sha1_checksum, |
| pool); |
| const char *rep_string = representation_string(noderev->data_rep, |
| ffd->format, |
| (noderev->kind |
| == svn_node_dir), |
| FALSE, |
| pool); |
| SVN_ERR(svn_io_file_open(&rep_file, file_name, |
| APR_WRITE | APR_CREATE | APR_TRUNCATE |
| | APR_BUFFERED, APR_OS_DEFAULT, pool)); |
| |
| SVN_ERR(svn_io_file_write_full(rep_file, rep_string, |
| strlen(rep_string), NULL, pool)); |
| |
| SVN_ERR(svn_io_file_close(rep_file, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* This structure is used to hold the information associated with a |
| REP line. */ |
| struct rep_args |
| { |
| svn_boolean_t is_delta; |
| svn_boolean_t is_delta_vs_empty; |
| |
| svn_revnum_t base_revision; |
| apr_off_t base_offset; |
| svn_filesize_t base_length; |
| }; |
| |
| /* Read the next line from file FILE and parse it as a text |
| representation entry. Return the parsed entry in *REP_ARGS_P. |
| Perform all allocations in POOL. */ |
| static svn_error_t * |
| read_rep_line(struct rep_args **rep_args_p, |
| apr_file_t *file, |
| apr_pool_t *pool) |
| { |
| char buffer[160]; |
| apr_size_t limit; |
| struct rep_args *rep_args; |
| char *str, *last_str = buffer; |
| apr_int64_t val; |
| |
| limit = sizeof(buffer); |
| SVN_ERR(svn_io_read_length_line(file, buffer, &limit, pool)); |
| |
| rep_args = apr_pcalloc(pool, sizeof(*rep_args)); |
| rep_args->is_delta = FALSE; |
| |
| if (strcmp(buffer, REP_PLAIN) == 0) |
| { |
| *rep_args_p = rep_args; |
| return SVN_NO_ERROR; |
| } |
| |
| if (strcmp(buffer, REP_DELTA) == 0) |
| { |
| /* This is a delta against the empty stream. */ |
| rep_args->is_delta = TRUE; |
| rep_args->is_delta_vs_empty = TRUE; |
| *rep_args_p = rep_args; |
| return SVN_NO_ERROR; |
| } |
| |
| rep_args->is_delta = TRUE; |
| rep_args->is_delta_vs_empty = FALSE; |
| |
| /* We have hopefully a DELTA vs. a non-empty base revision. */ |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (! str || (strcmp(str, REP_DELTA) != 0)) |
| goto error; |
| |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (! str) |
| goto error; |
| rep_args->base_revision = SVN_STR_TO_REV(str); |
| |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (! str) |
| goto error; |
| SVN_ERR(svn_cstring_atoi64(&val, str)); |
| rep_args->base_offset = (apr_off_t)val; |
| |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (! str) |
| goto error; |
| SVN_ERR(svn_cstring_atoi64(&val, str)); |
| rep_args->base_length = (svn_filesize_t)val; |
| |
| *rep_args_p = rep_args; |
| return SVN_NO_ERROR; |
| |
| error: |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed representation header at %s"), |
| path_and_offset_of(file, pool)); |
| } |
| |
| /* Given a revision file REV_FILE, opened to REV in FS, find the Node-ID |
| of the header located at OFFSET and store it in *ID_P. Allocate |
| temporary variables from POOL. */ |
| static svn_error_t * |
| get_fs_id_at_offset(svn_fs_id_t **id_p, |
| apr_file_t *rev_file, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_off_t offset, |
| apr_pool_t *pool) |
| { |
| svn_fs_id_t *id; |
| apr_hash_t *headers; |
| const char *node_id_str; |
| |
| SVN_ERR(svn_io_file_seek(rev_file, APR_SET, &offset, pool)); |
| |
| SVN_ERR(read_header_block(&headers, |
| svn_stream_from_aprfile2(rev_file, TRUE, pool), |
| pool)); |
| |
| /* In error messages, the offset is relative to the pack file, |
| not to the rev file. */ |
| |
| node_id_str = svn_hash_gets(headers, HEADER_ID); |
| |
| if (node_id_str == NULL) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Missing node-id in node-rev at r%ld " |
| "(offset %s)"), |
| rev, |
| apr_psprintf(pool, "%" APR_OFF_T_FMT, offset)); |
| |
| id = svn_fs_fs__id_parse(node_id_str, strlen(node_id_str), pool); |
| |
| if (id == NULL) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Corrupt node-id '%s' in node-rev at r%ld " |
| "(offset %s)"), |
| node_id_str, rev, |
| apr_psprintf(pool, "%" APR_OFF_T_FMT, offset)); |
| |
| *id_p = id; |
| |
| /* ### assert that the txn_id is REV/OFFSET ? */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Given an open revision file REV_FILE in FS for REV, locate the trailer that |
| specifies the offset to the root node-id and to the changed path |
| information. Store the root node offset in *ROOT_OFFSET and the |
| changed path offset in *CHANGES_OFFSET. If either of these |
| pointers is NULL, do nothing with it. |
| |
| If PACKED is true, REV_FILE should be a packed shard file. |
| ### There is currently no such parameter. This function assumes that |
| is_packed_rev(FS, REV) will indicate whether REV_FILE is a packed |
| file. Therefore FS->fsap_data->min_unpacked_rev must not have been |
| refreshed since REV_FILE was opened if there is a possibility that |
| revision REV may have become packed since then. |
| TODO: Take an IS_PACKED parameter instead, in order to remove this |
| requirement. |
| |
| Allocate temporary variables from POOL. */ |
| static svn_error_t * |
| get_root_changes_offset(apr_off_t *root_offset, |
| apr_off_t *changes_offset, |
| apr_file_t *rev_file, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| apr_off_t offset; |
| apr_off_t rev_offset; |
| char buf[64]; |
| int i, num_bytes; |
| const char *str; |
| apr_size_t len; |
| apr_seek_where_t seek_relative; |
| |
| /* Determine where to seek to in the file. |
| |
| If we've got a pack file, we want to seek to the end of the desired |
| revision. But we don't track that, so we seek to the beginning of the |
| next revision. |
| |
| Unless the next revision is in a different file, in which case, we can |
| just seek to the end of the pack file -- just like we do in the |
| non-packed case. */ |
| if (is_packed_rev(fs, rev) && ((rev + 1) % ffd->max_files_per_dir != 0)) |
| { |
| SVN_ERR(get_packed_offset(&offset, fs, rev + 1, pool)); |
| seek_relative = APR_SET; |
| } |
| else |
| { |
| seek_relative = APR_END; |
| offset = 0; |
| } |
| |
| /* Offset of the revision from the start of the pack file, if applicable. */ |
| if (is_packed_rev(fs, rev)) |
| SVN_ERR(get_packed_offset(&rev_offset, fs, rev, pool)); |
| else |
| rev_offset = 0; |
| |
| /* We will assume that the last line containing the two offsets |
| will never be longer than 64 characters. */ |
| SVN_ERR(svn_io_file_seek(rev_file, seek_relative, &offset, pool)); |
| |
| offset -= sizeof(buf); |
| SVN_ERR(svn_io_file_seek(rev_file, APR_SET, &offset, pool)); |
| |
| /* Read in this last block, from which we will identify the last line. */ |
| len = sizeof(buf); |
| SVN_ERR(svn_io_file_read(rev_file, buf, &len, pool)); |
| |
| /* This cast should be safe since the maximum amount read, 64, will |
| never be bigger than the size of an int. */ |
| num_bytes = (int) len; |
| |
| /* The last byte should be a newline. */ |
| if (buf[num_bytes - 1] != '\n') |
| { |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Revision file (r%ld) lacks trailing newline"), |
| rev); |
| } |
| |
| /* Look for the next previous newline. */ |
| for (i = num_bytes - 2; i >= 0; i--) |
| { |
| if (buf[i] == '\n') |
| break; |
| } |
| |
| if (i < 0) |
| { |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Final line in revision file (r%ld) longer " |
| "than 64 characters"), |
| rev); |
| } |
| |
| i++; |
| str = &buf[i]; |
| |
| /* find the next space */ |
| for ( ; i < (num_bytes - 2) ; i++) |
| if (buf[i] == ' ') |
| break; |
| |
| if (i == (num_bytes - 2)) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Final line in revision file r%ld missing space"), |
| rev); |
| |
| if (root_offset) |
| { |
| apr_int64_t val; |
| |
| buf[i] = '\0'; |
| SVN_ERR(svn_cstring_atoi64(&val, str)); |
| *root_offset = rev_offset + (apr_off_t)val; |
| } |
| |
| i++; |
| str = &buf[i]; |
| |
| /* find the next newline */ |
| for ( ; i < num_bytes; i++) |
| if (buf[i] == '\n') |
| break; |
| |
| if (changes_offset) |
| { |
| apr_int64_t val; |
| |
| buf[i] = '\0'; |
| SVN_ERR(svn_cstring_atoi64(&val, str)); |
| *changes_offset = rev_offset + (apr_off_t)val; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Move a file into place from OLD_FILENAME in the transactions |
| directory to its final location NEW_FILENAME in the repository. On |
| Unix, match the permissions of the new file to the permissions of |
| PERMS_REFERENCE. Temporary allocations are from POOL. |
| |
| This function almost duplicates svn_io_file_move(), but it tries to |
| guarantee a flush. */ |
| static svn_error_t * |
| move_into_place(const char *old_filename, |
| const char *new_filename, |
| const char *perms_reference, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| |
| SVN_ERR(svn_io_copy_perms(perms_reference, old_filename, pool)); |
| |
| /* Move the file into place. */ |
| err = svn_io_file_rename(old_filename, new_filename, pool); |
| if (err && APR_STATUS_IS_EXDEV(err->apr_err)) |
| { |
| apr_file_t *file; |
| |
| /* Can't rename across devices; fall back to copying. */ |
| svn_error_clear(err); |
| err = SVN_NO_ERROR; |
| SVN_ERR(svn_io_copy_file(old_filename, new_filename, TRUE, pool)); |
| |
| /* Flush the target of the copy to disk. */ |
| SVN_ERR(svn_io_file_open(&file, new_filename, APR_READ, |
| APR_OS_DEFAULT, pool)); |
| /* ### BH: Does this really guarantee a flush of the data written |
| ### via a completely different handle on all operating systems? |
| ### |
| ### Maybe we should perform the copy ourselves instead of making |
| ### apr do that and flush the real handle? */ |
| SVN_ERR(svn_io_file_flush_to_disk(file, pool)); |
| SVN_ERR(svn_io_file_close(file, pool)); |
| } |
| if (err) |
| return svn_error_trace(err); |
| |
| #ifdef __linux__ |
| { |
| /* Linux has the unusual feature that fsync() on a file is not |
| enough to ensure that a file's directory entries have been |
| flushed to disk; you have to fsync the directory as well. |
| On other operating systems, we'd only be asking for trouble |
| by trying to open and fsync a directory. */ |
| const char *dirname; |
| apr_file_t *file; |
| |
| dirname = svn_dirent_dirname(new_filename, pool); |
| SVN_ERR(svn_io_file_open(&file, dirname, APR_READ, APR_OS_DEFAULT, |
| pool)); |
| SVN_ERR(svn_io_file_flush_to_disk(file, pool)); |
| SVN_ERR(svn_io_file_close(file, pool)); |
| } |
| #endif |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__rev_get_root(svn_fs_id_t **root_id_p, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| apr_file_t *revision_file; |
| apr_off_t root_offset; |
| svn_fs_id_t *root_id = NULL; |
| svn_boolean_t is_cached; |
| |
| SVN_ERR(ensure_revision_exists(fs, rev, pool)); |
| |
| SVN_ERR(svn_cache__get((void **) root_id_p, &is_cached, |
| ffd->rev_root_id_cache, &rev, pool)); |
| if (is_cached) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(open_pack_or_rev_file(&revision_file, fs, rev, pool)); |
| SVN_ERR(get_root_changes_offset(&root_offset, NULL, revision_file, fs, rev, |
| pool)); |
| |
| SVN_ERR(get_fs_id_at_offset(&root_id, revision_file, fs, rev, |
| root_offset, pool)); |
| |
| SVN_ERR(svn_io_file_close(revision_file, pool)); |
| |
| SVN_ERR(svn_cache__set(ffd->rev_root_id_cache, &rev, root_id, pool)); |
| |
| *root_id_p = root_id; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Revprop caching management. |
| * |
| * Mechanism: |
| * ---------- |
| * |
| * Revprop caching needs to be activated and will be deactivated for the |
| * respective FS instance if the necessary infrastructure could not be |
| * initialized. In deactivated mode, there is almost no runtime overhead |
| * associated with revprop caching. As long as no revprops are being read |
| * or changed, revprop caching imposes no overhead. |
| * |
| * When activated, we cache revprops using (revision, generation) pairs |
| * as keys with the generation being incremented upon every revprop change. |
| * Since the cache is process-local, the generation needs to be tracked |
| * for at least as long as the process lives but may be reset afterwards. |
| * |
| * To track the revprop generation, we use two-layer approach. On the lower |
| * level, we use named atomics to have a system-wide consistent value for |
| * the current revprop generation. However, those named atomics will only |
| * remain valid for as long as at least one process / thread in the system |
| * accesses revprops in the respective repository. The underlying shared |
| * memory gets cleaned up afterwards. |
| * |
| * On the second level, we will use a persistent file to track the latest |
| * revprop generation. It will be written upon each revprop change but |
| * only be read if we are the first process to initialize the named atomics |
| * with that value. |
| * |
| * The overhead for the second and following accesses to revprops is |
| * almost zero on most systems. |
| * |
| * |
| * Tech aspects: |
| * ------------- |
| * |
| * A problem is that we need to provide a globally available file name to |
| * back the SHM implementation on OSes that need it. We can only assume |
| * write access to some file within the respective repositories. Because |
| * a given server process may access thousands of repositories during its |
| * lifetime, keeping the SHM data alive for all of them is also not an |
| * option. |
| * |
| * So, we store the new revprop generation on disk as part of each |
| * setrevprop call, i.e. this write will be serialized and the write order |
| * be guaranteed by the repository write lock. |
| * |
| * The only racy situation occurs when the data is being read again by two |
| * processes concurrently but in that situation, the first process to |
| * finish that procedure is guaranteed to be the only one that initializes |
| * the SHM data. Since even writers will first go through that |
| * initialization phase, they will never operate on stale data. |
| */ |
| |
| /* Read revprop generation as stored on disk for repository FS. The result |
| * is returned in *CURRENT. Default to 2 if no such file is available. |
| */ |
| static svn_error_t * |
| read_revprop_generation_file(apr_int64_t *current, |
| svn_fs_t *fs, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| apr_file_t *file; |
| char buf[80]; |
| apr_size_t len; |
| const char *path = path_revprop_generation(fs, pool); |
| |
| err = svn_io_file_open(&file, path, |
| APR_READ | APR_BUFFERED, |
| APR_OS_DEFAULT, pool); |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| svn_error_clear(err); |
| *current = 2; |
| |
| return SVN_NO_ERROR; |
| } |
| SVN_ERR(err); |
| |
| len = sizeof(buf); |
| SVN_ERR(svn_io_read_length_line(file, buf, &len, pool)); |
| |
| /* Check that the first line contains only digits. */ |
| SVN_ERR(check_file_buffer_numeric(buf, 0, path, |
| "Revprop Generation", pool)); |
| SVN_ERR(svn_cstring_atoi64(current, buf)); |
| |
| return svn_io_file_close(file, pool); |
| } |
| |
| /* Write the CURRENT revprop generation to disk for repository FS. |
| */ |
| static svn_error_t * |
| write_revprop_generation_file(svn_fs_t *fs, |
| apr_int64_t current, |
| apr_pool_t *pool) |
| { |
| apr_file_t *file; |
| const char *tmp_path; |
| |
| char buf[SVN_INT64_BUFFER_SIZE]; |
| apr_size_t len = svn__i64toa(buf, current); |
| buf[len] = '\n'; |
| |
| SVN_ERR(svn_io_open_unique_file3(&file, &tmp_path, fs->path, |
| svn_io_file_del_none, pool, pool)); |
| SVN_ERR(svn_io_file_write_full(file, buf, len + 1, NULL, pool)); |
| SVN_ERR(svn_io_file_close(file, pool)); |
| |
| return move_into_place(tmp_path, path_revprop_generation(fs, pool), |
| tmp_path, pool); |
| } |
| |
| /* Make sure the revprop_namespace member in FS is set. */ |
| static svn_error_t * |
| ensure_revprop_namespace(svn_fs_t *fs) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| return ffd->revprop_namespace == NULL |
| ? svn_atomic_namespace__create(&ffd->revprop_namespace, |
| svn_dirent_join(fs->path, |
| ATOMIC_REVPROP_NAMESPACE, |
| fs->pool), |
| fs->pool) |
| : SVN_NO_ERROR; |
| } |
| |
| /* Make sure the revprop_namespace member in FS is set. */ |
| static svn_error_t * |
| cleanup_revprop_namespace(svn_fs_t *fs) |
| { |
| const char *name = svn_dirent_join(fs->path, |
| ATOMIC_REVPROP_NAMESPACE, |
| fs->pool); |
| return svn_error_trace(svn_atomic_namespace__cleanup(name, fs->pool)); |
| } |
| |
| /* Make sure the revprop_generation member in FS is set and, if necessary, |
| * initialized with the latest value stored on disk. |
| */ |
| static svn_error_t * |
| ensure_revprop_generation(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| SVN_ERR(ensure_revprop_namespace(fs)); |
| if (ffd->revprop_generation == NULL) |
| { |
| apr_int64_t current = 0; |
| |
| SVN_ERR(svn_named_atomic__get(&ffd->revprop_generation, |
| ffd->revprop_namespace, |
| ATOMIC_REVPROP_GENERATION, |
| TRUE)); |
| |
| /* If the generation is at 0, we just created a new namespace |
| * (it would be at least 2 otherwise). Read the latest generation |
| * from disk and if we are the first one to initialize the atomic |
| * (i.e. is still 0), set it to the value just gotten. |
| */ |
| SVN_ERR(svn_named_atomic__read(¤t, ffd->revprop_generation)); |
| if (current == 0) |
| { |
| SVN_ERR(read_revprop_generation_file(¤t, fs, pool)); |
| SVN_ERR(svn_named_atomic__cmpxchg(NULL, current, 0, |
| ffd->revprop_generation)); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Make sure the revprop_timeout member in FS is set. */ |
| static svn_error_t * |
| ensure_revprop_timeout(svn_fs_t *fs) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| SVN_ERR(ensure_revprop_namespace(fs)); |
| return ffd->revprop_timeout == NULL |
| ? svn_named_atomic__get(&ffd->revprop_timeout, |
| ffd->revprop_namespace, |
| ATOMIC_REVPROP_TIMEOUT, |
| TRUE) |
| : SVN_NO_ERROR; |
| } |
| |
| /* Create an error object with the given MESSAGE and pass it to the |
| WARNING member of FS. */ |
| static void |
| log_revprop_cache_init_warning(svn_fs_t *fs, |
| svn_error_t *underlying_err, |
| const char *message) |
| { |
| svn_error_t *err = svn_error_createf(SVN_ERR_FS_REVPROP_CACHE_INIT_FAILURE, |
| underlying_err, |
| message, fs->path); |
| |
| if (fs->warning) |
| (fs->warning)(fs->warning_baton, err); |
| |
| svn_error_clear(err); |
| } |
| |
| /* Test whether revprop cache and necessary infrastructure are |
| available in FS. */ |
| static svn_boolean_t |
| has_revprop_cache(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_error_t *error; |
| |
| /* is the cache (still) enabled? */ |
| if (ffd->revprop_cache == NULL) |
| return FALSE; |
| |
| /* is it efficient? */ |
| if (!svn_named_atomic__is_efficient()) |
| { |
| /* access to it would be quite slow |
| * -> disable the revprop cache for good |
| */ |
| ffd->revprop_cache = NULL; |
| log_revprop_cache_init_warning(fs, NULL, |
| "Revprop caching for '%s' disabled" |
| " because it would be inefficient."); |
| |
| return FALSE; |
| } |
| |
| /* try to access our SHM-backed infrastructure */ |
| error = ensure_revprop_generation(fs, pool); |
| if (error) |
| { |
| /* failure -> disable revprop cache for good */ |
| |
| ffd->revprop_cache = NULL; |
| log_revprop_cache_init_warning(fs, error, |
| "Revprop caching for '%s' disabled " |
| "because SHM infrastructure for revprop " |
| "caching failed to initialize."); |
| |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| /* Baton structure for revprop_generation_fixup. */ |
| typedef struct revprop_generation_fixup_t |
| { |
| /* revprop generation to read */ |
| apr_int64_t *generation; |
| |
| /* containing the revprop_generation member to query */ |
| fs_fs_data_t *ffd; |
| } revprop_generation_upgrade_t; |
| |
| /* If the revprop generation has an odd value, it means the original writer |
| of the revprop got killed. We don't know whether that process as able |
| to change the revprop data but we assume that it was. Therefore, we |
| increase the generation in that case to basically invalidate everyones |
| cache content. |
| Execute this onlx while holding the write lock to the repo in baton->FFD. |
| */ |
| static svn_error_t * |
| revprop_generation_fixup(void *void_baton, |
| apr_pool_t *pool) |
| { |
| revprop_generation_upgrade_t *baton = void_baton; |
| assert(baton->ffd->has_write_lock); |
| |
| /* Maybe, either the original revprop writer or some other reader has |
| already corrected / bumped the revprop generation. Thus, we need |
| to read it again. */ |
| SVN_ERR(svn_named_atomic__read(baton->generation, |
| baton->ffd->revprop_generation)); |
| |
| /* Cause everyone to re-read revprops upon their next access, if the |
| last revprop write did not complete properly. */ |
| while (*baton->generation % 2) |
| SVN_ERR(svn_named_atomic__add(baton->generation, |
| 1, |
| baton->ffd->revprop_generation)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Read the current revprop generation and return it in *GENERATION. |
| Also, detect aborted / crashed writers and recover from that. |
| Use the access object in FS to set the shared mem values. */ |
| static svn_error_t * |
| read_revprop_generation(apr_int64_t *generation, |
| svn_fs_t *fs, |
| apr_pool_t *pool) |
| { |
| apr_int64_t current = 0; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* read the current revprop generation number */ |
| SVN_ERR(ensure_revprop_generation(fs, pool)); |
| SVN_ERR(svn_named_atomic__read(¤t, ffd->revprop_generation)); |
| |
| /* is an unfinished revprop write under the way? */ |
| if (current % 2) |
| { |
| apr_int64_t timeout = 0; |
| |
| /* read timeout for the write operation */ |
| SVN_ERR(ensure_revprop_timeout(fs)); |
| SVN_ERR(svn_named_atomic__read(&timeout, ffd->revprop_timeout)); |
| |
| /* has the writer process been aborted, |
| * i.e. has the timeout been reached? |
| */ |
| if (apr_time_now() > timeout) |
| { |
| revprop_generation_upgrade_t baton; |
| baton.generation = ¤t; |
| baton.ffd = ffd; |
| |
| /* Ensure that the original writer process no longer exists by |
| * acquiring the write lock to this repository. Then, fix up |
| * the revprop generation. |
| */ |
| if (ffd->has_write_lock) |
| SVN_ERR(revprop_generation_fixup(&baton, pool)); |
| else |
| SVN_ERR(svn_fs_fs__with_write_lock(fs, revprop_generation_fixup, |
| &baton, pool)); |
| } |
| } |
| |
| /* return the value we just got */ |
| *generation = current; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Set the revprop generation to the next odd number to indicate that |
| there is a revprop write process under way. If that times out, |
| readers shall recover from that state & re-read revprops. |
| Use the access object in FS to set the shared mem value. */ |
| static svn_error_t * |
| begin_revprop_change(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| apr_int64_t current; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* set the timeout for the write operation */ |
| SVN_ERR(ensure_revprop_timeout(fs)); |
| SVN_ERR(svn_named_atomic__write(NULL, |
| apr_time_now() + REVPROP_CHANGE_TIMEOUT, |
| ffd->revprop_timeout)); |
| |
| /* set the revprop generation to an odd value to indicate |
| * that a write is in progress |
| */ |
| SVN_ERR(ensure_revprop_generation(fs, pool)); |
| do |
| { |
| SVN_ERR(svn_named_atomic__add(¤t, |
| 1, |
| ffd->revprop_generation)); |
| } |
| while (current % 2 == 0); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Set the revprop generation to the next even number to indicate that |
| a) readers shall re-read revprops, and |
| b) the write process has been completed (no recovery required) |
| Use the access object in FS to set the shared mem value. */ |
| static svn_error_t * |
| end_revprop_change(svn_fs_t *fs, apr_pool_t *pool) |
| { |
| apr_int64_t current = 1; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* set the revprop generation to an even value to indicate |
| * that a write has been completed |
| */ |
| SVN_ERR(ensure_revprop_generation(fs, pool)); |
| do |
| { |
| SVN_ERR(svn_named_atomic__add(¤t, |
| 1, |
| ffd->revprop_generation)); |
| } |
| while (current % 2); |
| |
| /* Save the latest generation to disk. FS is currently in a "locked" |
| * state such that we can be sure the be the only ones to write that |
| * file. |
| */ |
| return write_revprop_generation_file(fs, current, pool); |
| } |
| |
| /* Container for all data required to access the packed revprop file |
| * for a given REVISION. This structure will be filled incrementally |
| * by read_pack_revprops() its sub-routines. |
| */ |
| typedef struct packed_revprops_t |
| { |
| /* revision number to read (not necessarily the first in the pack) */ |
| svn_revnum_t revision; |
| |
| /* current revprop generation. Used when populating the revprop cache */ |
| apr_int64_t generation; |
| |
| /* the actual revision properties */ |
| apr_hash_t *properties; |
| |
| /* their size when serialized to a single string |
| * (as found in PACKED_REVPROPS) */ |
| apr_size_t serialized_size; |
| |
| |
| /* name of the pack file (without folder path) */ |
| const char *filename; |
| |
| /* packed shard folder path */ |
| const char *folder; |
| |
| /* sum of values in SIZES */ |
| apr_size_t total_size; |
| |
| /* first revision in the pack (>= MANIFEST_START) */ |
| svn_revnum_t start_revision; |
| |
| /* size of the revprops in PACKED_REVPROPS */ |
| apr_array_header_t *sizes; |
| |
| /* offset of the revprops in PACKED_REVPROPS */ |
| apr_array_header_t *offsets; |
| |
| |
| /* concatenation of the serialized representation of all revprops |
| * in the pack, i.e. the pack content without header and compression */ |
| svn_stringbuf_t *packed_revprops; |
| |
| /* First revision covered by MANIFEST. |
| * Will equal the shard start revision or 1, for the 1st shard. */ |
| svn_revnum_t manifest_start; |
| |
| /* content of the manifest. |
| * Maps long(rev - MANIFEST_START) to const char* pack file name */ |
| apr_array_header_t *manifest; |
| } packed_revprops_t; |
| |
| /* Parse the serialized revprops in CONTENT and return them in *PROPERTIES. |
| * Also, put them into the revprop cache, if activated, for future use. |
| * Three more parameters are being used to update the revprop cache: FS is |
| * our file system, the revprops belong to REVISION and the global revprop |
| * GENERATION is used as well. |
| * |
| * The returned hash will be allocated in POOL, SCRATCH_POOL is being used |
| * for temporary allocations. |
| */ |
| static svn_error_t * |
| parse_revprop(apr_hash_t **properties, |
| svn_fs_t *fs, |
| svn_revnum_t revision, |
| apr_int64_t generation, |
| svn_string_t *content, |
| apr_pool_t *pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_stream_t *stream = svn_stream_from_string(content, scratch_pool); |
| *properties = apr_hash_make(pool); |
| |
| SVN_ERR(svn_hash_read2(*properties, stream, SVN_HASH_TERMINATOR, pool)); |
| if (has_revprop_cache(fs, pool)) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| pair_cache_key_t key = { 0 }; |
| |
| key.revision = revision; |
| key.second = generation; |
| SVN_ERR(svn_cache__set(ffd->revprop_cache, &key, *properties, |
| scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Read the non-packed revprops for revision REV in FS, put them into the |
| * revprop cache if activated and return them in *PROPERTIES. GENERATION |
| * is the current revprop generation. |
| * |
| * If the data could not be read due to an otherwise recoverable error, |
| * leave *PROPERTIES unchanged. No error will be returned in that case. |
| * |
| * Allocations will be done in POOL. |
| */ |
| static svn_error_t * |
| read_non_packed_revprop(apr_hash_t **properties, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_int64_t generation, |
| apr_pool_t *pool) |
| { |
| svn_stringbuf_t *content = NULL; |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| svn_boolean_t missing = FALSE; |
| int i; |
| |
| for (i = 0; i < RECOVERABLE_RETRY_COUNT && !missing && !content; ++i) |
| { |
| svn_pool_clear(iterpool); |
| SVN_ERR(try_stringbuf_from_file(&content, |
| &missing, |
| path_revprops(fs, rev, iterpool), |
| i + 1 < RECOVERABLE_RETRY_COUNT, |
| iterpool)); |
| } |
| |
| if (content) |
| SVN_ERR(parse_revprop(properties, fs, rev, generation, |
| svn_stringbuf__morph_into_string(content), |
| pool, iterpool)); |
| |
| svn_pool_clear(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Given FS and REVPROPS->REVISION, fill the FILENAME, FOLDER and MANIFEST |
| * members. Use POOL for allocating results and SCRATCH_POOL for temporaries. |
| */ |
| static svn_error_t * |
| get_revprop_packname(svn_fs_t *fs, |
| packed_revprops_t *revprops, |
| apr_pool_t *pool, |
| apr_pool_t *scratch_pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_stringbuf_t *content = NULL; |
| const char *manifest_file_path; |
| int idx; |
| |
| /* read content of the manifest file */ |
| revprops->folder = path_revprops_pack_shard(fs, revprops->revision, pool); |
| manifest_file_path = svn_dirent_join(revprops->folder, PATH_MANIFEST, pool); |
| |
| SVN_ERR(read_content(&content, manifest_file_path, pool)); |
| |
| /* parse the manifest. Every line is a file name */ |
| revprops->manifest = apr_array_make(pool, ffd->max_files_per_dir, |
| sizeof(const char*)); |
| |
| /* Read all lines. Since the last line ends with a newline, we will |
| end up with a valid but empty string after the last entry. */ |
| while (content->data && *content->data) |
| { |
| APR_ARRAY_PUSH(revprops->manifest, const char*) = content->data; |
| content->data = strchr(content->data, '\n'); |
| if (content->data) |
| { |
| *content->data = 0; |
| content->data++; |
| } |
| } |
| |
| /* Index for our revision. Rev 0 is excluded from the first shard. */ |
| revprops->manifest_start = revprops->revision |
| - (revprops->revision % ffd->max_files_per_dir); |
| if (revprops->manifest_start == 0) |
| ++revprops->manifest_start; |
| idx = (int)(revprops->revision - revprops->manifest_start); |
| |
| if (revprops->manifest->nelts <= idx) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Packed revprop manifest for r%ld too " |
| "small"), revprops->revision); |
| |
| /* Now get the file name */ |
| revprops->filename = APR_ARRAY_IDX(revprops->manifest, idx, const char*); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return TRUE, if revision R1 and R2 refer to the same shard in FS. |
| */ |
| static svn_boolean_t |
| same_shard(svn_fs_t *fs, |
| svn_revnum_t r1, |
| svn_revnum_t r2) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| return (r1 / ffd->max_files_per_dir) == (r2 / ffd->max_files_per_dir); |
| } |
| |
| /* Given FS and the full packed file content in REVPROPS->PACKED_REVPROPS, |
| * fill the START_REVISION, SIZES, OFFSETS members. Also, make |
| * PACKED_REVPROPS point to the first serialized revprop. |
| * |
| * Parse the revprops for REVPROPS->REVISION and set the PROPERTIES as |
| * well as the SERIALIZED_SIZE member. If revprop caching has been |
| * enabled, parse all revprops in the pack and cache them. |
| */ |
| static svn_error_t * |
| parse_packed_revprops(svn_fs_t *fs, |
| packed_revprops_t *revprops, |
| apr_pool_t *pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_stream_t *stream; |
| apr_int64_t first_rev, count, i; |
| apr_off_t offset; |
| const char *header_end; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| |
| /* decompress (even if the data is only "stored", there is still a |
| * length header to remove) */ |
| svn_string_t *compressed |
| = svn_stringbuf__morph_into_string(revprops->packed_revprops); |
| svn_stringbuf_t *uncompressed = svn_stringbuf_create_empty(pool); |
| SVN_ERR(svn__decompress(compressed, uncompressed, APR_SIZE_MAX)); |
| |
| /* read first revision number and number of revisions in the pack */ |
| stream = svn_stream_from_stringbuf(uncompressed, scratch_pool); |
| SVN_ERR(read_number_from_stream(&first_rev, NULL, stream, iterpool)); |
| SVN_ERR(read_number_from_stream(&count, NULL, stream, iterpool)); |
| |
| /* Check revision range for validity. */ |
| if ( !same_shard(fs, revprops->revision, first_rev) |
| || !same_shard(fs, revprops->revision, first_rev + count - 1) |
| || count < 1) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Revprop pack for revision r%ld" |
| " contains revprops for r%ld .. r%ld"), |
| revprops->revision, |
| (svn_revnum_t)first_rev, |
| (svn_revnum_t)(first_rev + count -1)); |
| |
| /* Since start & end are in the same shard, it is enough to just test |
| * the FIRST_REV for being actually packed. That will also cover the |
| * special case of rev 0 never being packed. */ |
| if (!is_packed_revprop(fs, first_rev)) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Revprop pack for revision r%ld" |
| " starts at non-packed revisions r%ld"), |
| revprops->revision, (svn_revnum_t)first_rev); |
| |
| /* make PACKED_REVPROPS point to the first char after the header. |
| * This is where the serialized revprops are. */ |
| header_end = strstr(uncompressed->data, "\n\n"); |
| if (header_end == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Header end not found")); |
| |
| offset = header_end - uncompressed->data + 2; |
| |
| revprops->packed_revprops = svn_stringbuf_create_empty(pool); |
| revprops->packed_revprops->data = uncompressed->data + offset; |
| revprops->packed_revprops->len = (apr_size_t)(uncompressed->len - offset); |
| revprops->packed_revprops->blocksize = (apr_size_t)(uncompressed->blocksize - offset); |
| |
| /* STREAM still points to the first entry in the sizes list. |
| * Init / construct REVPROPS members. */ |
| revprops->start_revision = (svn_revnum_t)first_rev; |
| revprops->sizes = apr_array_make(pool, (int)count, sizeof(offset)); |
| revprops->offsets = apr_array_make(pool, (int)count, sizeof(offset)); |
| |
| /* Now parse, revision by revision, the size and content of each |
| * revisions' revprops. */ |
| for (i = 0, offset = 0, revprops->total_size = 0; i < count; ++i) |
| { |
| apr_int64_t size; |
| svn_string_t serialized; |
| apr_hash_t *properties; |
| svn_revnum_t revision = (svn_revnum_t)(first_rev + i); |
| |
| /* read & check the serialized size */ |
| SVN_ERR(read_number_from_stream(&size, NULL, stream, iterpool)); |
| if (size + offset > (apr_int64_t)revprops->packed_revprops->len) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Packed revprop size exceeds pack file size")); |
| |
| /* Parse this revprops list, if necessary */ |
| serialized.data = revprops->packed_revprops->data + offset; |
| serialized.len = (apr_size_t)size; |
| |
| if (revision == revprops->revision) |
| { |
| SVN_ERR(parse_revprop(&revprops->properties, fs, revision, |
| revprops->generation, &serialized, |
| pool, iterpool)); |
| revprops->serialized_size = serialized.len; |
| } |
| else |
| { |
| /* If revprop caching is enabled, parse any revprops. |
| * They will get cached as a side-effect of this. */ |
| if (has_revprop_cache(fs, pool)) |
| SVN_ERR(parse_revprop(&properties, fs, revision, |
| revprops->generation, &serialized, |
| iterpool, iterpool)); |
| } |
| |
| /* fill REVPROPS data structures */ |
| APR_ARRAY_PUSH(revprops->sizes, apr_off_t) = serialized.len; |
| APR_ARRAY_PUSH(revprops->offsets, apr_off_t) = offset; |
| revprops->total_size += serialized.len; |
| |
| offset += serialized.len; |
| |
| svn_pool_clear(iterpool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* In filesystem FS, read the packed revprops for revision REV into |
| * *REVPROPS. Use GENERATION to populate the revprop cache, if enabled. |
| * Allocate data in POOL. |
| */ |
| static svn_error_t * |
| read_pack_revprop(packed_revprops_t **revprops, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_int64_t generation, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| svn_boolean_t missing = FALSE; |
| svn_error_t *err; |
| packed_revprops_t *result; |
| int i; |
| |
| /* someone insisted that REV is packed. Double-check if necessary */ |
| if (!is_packed_revprop(fs, rev)) |
| SVN_ERR(update_min_unpacked_rev(fs, iterpool)); |
| |
| if (!is_packed_revprop(fs, rev)) |
| return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| _("No such packed revision %ld"), rev); |
| |
| /* initialize the result data structure */ |
| result = apr_pcalloc(pool, sizeof(*result)); |
| result->revision = rev; |
| result->generation = generation; |
| |
| /* try to read the packed revprops. This may require retries if we have |
| * concurrent writers. */ |
| for (i = 0; i < RECOVERABLE_RETRY_COUNT && !result->packed_revprops; ++i) |
| { |
| const char *file_path; |
| |
| /* there might have been concurrent writes. |
| * Re-read the manifest and the pack file. |
| */ |
| SVN_ERR(get_revprop_packname(fs, result, pool, iterpool)); |
| file_path = svn_dirent_join(result->folder, |
| result->filename, |
| iterpool); |
| SVN_ERR(try_stringbuf_from_file(&result->packed_revprops, |
| &missing, |
| file_path, |
| i + 1 < RECOVERABLE_RETRY_COUNT, |
| pool)); |
| |
| /* If we could not find the file, there was a write. |
| * So, we should refresh our revprop generation info as well such |
| * that others may find data we will put into the cache. They would |
| * consider it outdated, otherwise. |
| */ |
| if (missing && has_revprop_cache(fs, pool)) |
| SVN_ERR(read_revprop_generation(&result->generation, fs, pool)); |
| |
| svn_pool_clear(iterpool); |
| } |
| |
| /* the file content should be available now */ |
| if (!result->packed_revprops) |
| return svn_error_createf(SVN_ERR_FS_PACKED_REVPROP_READ_FAILURE, NULL, |
| _("Failed to read revprop pack file for r%ld"), rev); |
| |
| /* parse it. RESULT will be complete afterwards. */ |
| err = parse_packed_revprops(fs, result, pool, iterpool); |
| svn_pool_destroy(iterpool); |
| if (err) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, err, |
| _("Revprop pack file for r%ld is corrupt"), rev); |
| |
| *revprops = result; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Read the revprops for revision REV in FS and return them in *PROPERTIES_P. |
| * |
| * Allocations will be done in POOL. |
| */ |
| static svn_error_t * |
| get_revision_proplist(apr_hash_t **proplist_p, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| apr_int64_t generation = 0; |
| |
| /* not found, yet */ |
| *proplist_p = NULL; |
| |
| /* should they be available at all? */ |
| SVN_ERR(ensure_revision_exists(fs, rev, pool)); |
| |
| /* Try cache lookup first. */ |
| if (has_revprop_cache(fs, pool)) |
| { |
| svn_boolean_t is_cached; |
| pair_cache_key_t key = { 0 }; |
| |
| SVN_ERR(read_revprop_generation(&generation, fs, pool)); |
| |
| key.revision = rev; |
| key.second = generation; |
| SVN_ERR(svn_cache__get((void **) proplist_p, &is_cached, |
| ffd->revprop_cache, &key, pool)); |
| if (is_cached) |
| return SVN_NO_ERROR; |
| } |
| |
| /* if REV had not been packed when we began, try reading it from the |
| * non-packed shard. If that fails, we will fall through to packed |
| * shard reads. */ |
| if (!is_packed_revprop(fs, rev)) |
| { |
| svn_error_t *err = read_non_packed_revprop(proplist_p, fs, rev, |
| generation, pool); |
| if (err) |
| { |
| if (!APR_STATUS_IS_ENOENT(err->apr_err) |
| || ffd->format < SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT) |
| return svn_error_trace(err); |
| |
| svn_error_clear(err); |
| *proplist_p = NULL; /* in case read_non_packed_revprop changed it */ |
| } |
| } |
| |
| /* if revprop packing is available and we have not read the revprops, yet, |
| * try reading them from a packed shard. If that fails, REV is most |
| * likely invalid (or its revprops highly contested). */ |
| if (ffd->format >= SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT && !*proplist_p) |
| { |
| packed_revprops_t *packed_revprops; |
| SVN_ERR(read_pack_revprop(&packed_revprops, fs, rev, generation, pool)); |
| *proplist_p = packed_revprops->properties; |
| } |
| |
| /* The revprops should have been there. Did we get them? */ |
| if (!*proplist_p) |
| return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| _("Could not read revprops for revision %ld"), |
| rev); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Serialize the revision property list PROPLIST of revision REV in |
| * filesystem FS to a non-packed file. Return the name of that temporary |
| * file in *TMP_PATH and the file path that it must be moved to in |
| * *FINAL_PATH. |
| * |
| * Use POOL for allocations. |
| */ |
| static svn_error_t * |
| write_non_packed_revprop(const char **final_path, |
| const char **tmp_path, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_hash_t *proplist, |
| apr_pool_t *pool) |
| { |
| apr_file_t *file; |
| svn_stream_t *stream; |
| *final_path = path_revprops(fs, rev, pool); |
| |
| /* ### do we have a directory sitting around already? we really shouldn't |
| ### have to get the dirname here. */ |
| SVN_ERR(svn_io_open_unique_file3(&file, tmp_path, |
| svn_dirent_dirname(*final_path, pool), |
| svn_io_file_del_none, pool, pool)); |
| stream = svn_stream_from_aprfile2(file, TRUE, pool); |
| SVN_ERR(svn_hash_write2(proplist, stream, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_stream_close(stream)); |
| |
| /* Flush temporary file to disk and close it. */ |
| SVN_ERR(svn_io_file_flush_to_disk(file, pool)); |
| SVN_ERR(svn_io_file_close(file, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* After writing the new revprop file(s), call this function to move the |
| * file at TMP_PATH to FINAL_PATH and give it the permissions from |
| * PERMS_REFERENCE. |
| * |
| * If indicated in BUMP_GENERATION, increase FS' revprop generation. |
| * Finally, delete all the temporary files given in FILES_TO_DELETE. |
| * The latter may be NULL. |
| * |
| * Use POOL for temporary allocations. |
| */ |
| static svn_error_t * |
| switch_to_new_revprop(svn_fs_t *fs, |
| const char *final_path, |
| const char *tmp_path, |
| const char *perms_reference, |
| apr_array_header_t *files_to_delete, |
| svn_boolean_t bump_generation, |
| apr_pool_t *pool) |
| { |
| /* Now, we may actually be replacing revprops. Make sure that all other |
| threads and processes will know about this. */ |
| if (bump_generation) |
| SVN_ERR(begin_revprop_change(fs, pool)); |
| |
| SVN_ERR(move_into_place(tmp_path, final_path, perms_reference, pool)); |
| |
| /* Indicate that the update (if relevant) has been completed. */ |
| if (bump_generation) |
| SVN_ERR(end_revprop_change(fs, pool)); |
| |
| /* Clean up temporary files, if necessary. */ |
| if (files_to_delete) |
| { |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| int i; |
| |
| for (i = 0; i < files_to_delete->nelts; ++i) |
| { |
| const char *path = APR_ARRAY_IDX(files_to_delete, i, const char*); |
| SVN_ERR(svn_io_remove_file2(path, TRUE, iterpool)); |
| svn_pool_clear(iterpool); |
| } |
| |
| svn_pool_destroy(iterpool); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write a pack file header to STREAM that starts at revision START_REVISION |
| * and contains the indexes [START,END) of SIZES. |
| */ |
| static svn_error_t * |
| serialize_revprops_header(svn_stream_t *stream, |
| svn_revnum_t start_revision, |
| apr_array_header_t *sizes, |
| int start, |
| int end, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| int i; |
| |
| SVN_ERR_ASSERT(start < end); |
| |
| /* start revision and entry count */ |
| SVN_ERR(svn_stream_printf(stream, pool, "%ld\n", start_revision)); |
| SVN_ERR(svn_stream_printf(stream, pool, "%d\n", end - start)); |
| |
| /* the sizes array */ |
| for (i = start; i < end; ++i) |
| { |
| apr_off_t size = APR_ARRAY_IDX(sizes, i, apr_off_t); |
| SVN_ERR(svn_stream_printf(stream, iterpool, "%" APR_OFF_T_FMT "\n", |
| size)); |
| } |
| |
| /* the double newline char indicates the end of the header */ |
| SVN_ERR(svn_stream_printf(stream, iterpool, "\n")); |
| |
| svn_pool_clear(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Writes the a pack file to FILE. It copies the serialized data |
| * from REVPROPS for the indexes [START,END) except for index CHANGED_INDEX. |
| * |
| * The data for the latter is taken from NEW_SERIALIZED. Note, that |
| * CHANGED_INDEX may be outside the [START,END) range, i.e. no new data is |
| * taken in that case but only a subset of the old data will be copied. |
| * |
| * NEW_TOTAL_SIZE is a hint for pre-allocating buffers of appropriate size. |
| * POOL is used for temporary allocations. |
| */ |
| static svn_error_t * |
| repack_revprops(svn_fs_t *fs, |
| packed_revprops_t *revprops, |
| int start, |
| int end, |
| int changed_index, |
| svn_stringbuf_t *new_serialized, |
| apr_off_t new_total_size, |
| apr_file_t *file, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_stream_t *stream; |
| int i; |
| |
| /* create data empty buffers and the stream object */ |
| svn_stringbuf_t *uncompressed |
| = svn_stringbuf_create_ensure((apr_size_t)new_total_size, pool); |
| svn_stringbuf_t *compressed |
| = svn_stringbuf_create_empty(pool); |
| stream = svn_stream_from_stringbuf(uncompressed, pool); |
| |
| /* write the header*/ |
| SVN_ERR(serialize_revprops_header(stream, revprops->start_revision + start, |
| revprops->sizes, start, end, pool)); |
| |
| /* append the serialized revprops */ |
| for (i = start; i < end; ++i) |
| if (i == changed_index) |
| { |
| SVN_ERR(svn_stream_write(stream, |
| new_serialized->data, |
| &new_serialized->len)); |
| } |
| else |
| { |
| apr_size_t size |
| = (apr_size_t)APR_ARRAY_IDX(revprops->sizes, i, apr_off_t); |
| apr_size_t offset |
| = (apr_size_t)APR_ARRAY_IDX(revprops->offsets, i, apr_off_t); |
| |
| SVN_ERR(svn_stream_write(stream, |
| revprops->packed_revprops->data + offset, |
| &size)); |
| } |
| |
| /* flush the stream buffer (if any) to our underlying data buffer */ |
| SVN_ERR(svn_stream_close(stream)); |
| |
| /* compress / store the data */ |
| SVN_ERR(svn__compress(svn_stringbuf__morph_into_string(uncompressed), |
| compressed, |
| ffd->compress_packed_revprops |
| ? SVN_DELTA_COMPRESSION_LEVEL_DEFAULT |
| : SVN_DELTA_COMPRESSION_LEVEL_NONE)); |
| |
| /* finally, write the content to the target file, flush and close it */ |
| SVN_ERR(svn_io_file_write_full(file, compressed->data, compressed->len, |
| NULL, pool)); |
| SVN_ERR(svn_io_file_flush_to_disk(file, pool)); |
| SVN_ERR(svn_io_file_close(file, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Allocate a new pack file name for revisions |
| * [REVPROPS->START_REVISION + START, REVPROPS->START_REVISION + END - 1] |
| * of REVPROPS->MANIFEST. Add the name of old file to FILES_TO_DELETE, |
| * auto-create that array if necessary. Return an open file *FILE that is |
| * allocated in POOL. |
| */ |
| static svn_error_t * |
| repack_file_open(apr_file_t **file, |
| svn_fs_t *fs, |
| packed_revprops_t *revprops, |
| int start, |
| int end, |
| apr_array_header_t **files_to_delete, |
| apr_pool_t *pool) |
| { |
| apr_int64_t tag; |
| const char *tag_string; |
| svn_string_t *new_filename; |
| int i; |
| int manifest_offset |
| = (int)(revprops->start_revision - revprops->manifest_start); |
| |
| /* get the old (= current) file name and enlist it for later deletion */ |
| const char *old_filename = APR_ARRAY_IDX(revprops->manifest, |
| start + manifest_offset, |
| const char*); |
| |
| if (*files_to_delete == NULL) |
| *files_to_delete = apr_array_make(pool, 3, sizeof(const char*)); |
| |
| APR_ARRAY_PUSH(*files_to_delete, const char*) |
| = svn_dirent_join(revprops->folder, old_filename, pool); |
| |
| /* increase the tag part, i.e. the counter after the dot */ |
| tag_string = strchr(old_filename, '.'); |
| if (tag_string == NULL) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Packed file '%s' misses a tag"), |
| old_filename); |
| |
| SVN_ERR(svn_cstring_atoi64(&tag, tag_string + 1)); |
| new_filename = svn_string_createf(pool, "%ld.%" APR_INT64_T_FMT, |
| revprops->start_revision + start, |
| ++tag); |
| |
| /* update the manifest to point to the new file */ |
| for (i = start; i < end; ++i) |
| APR_ARRAY_IDX(revprops->manifest, i + manifest_offset, const char*) |
| = new_filename->data; |
| |
| /* open the file */ |
| SVN_ERR(svn_io_file_open(file, svn_dirent_join(revprops->folder, |
| new_filename->data, |
| pool), |
| APR_WRITE | APR_CREATE, APR_OS_DEFAULT, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* For revision REV in filesystem FS, set the revision properties to |
| * PROPLIST. Return a new file in *TMP_PATH that the caller shall move |
| * to *FINAL_PATH to make the change visible. Files to be deleted will |
| * be listed in *FILES_TO_DELETE which may remain unchanged / unallocated. |
| * Use POOL for allocations. |
| */ |
| static svn_error_t * |
| write_packed_revprop(const char **final_path, |
| const char **tmp_path, |
| apr_array_header_t **files_to_delete, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_hash_t *proplist, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| packed_revprops_t *revprops; |
| apr_int64_t generation = 0; |
| svn_stream_t *stream; |
| apr_file_t *file; |
| svn_stringbuf_t *serialized; |
| apr_off_t new_total_size; |
| int changed_index; |
| |
| /* read the current revprop generation. This value will not change |
| * while we hold the global write lock to this FS. */ |
| if (has_revprop_cache(fs, pool)) |
| SVN_ERR(read_revprop_generation(&generation, fs, pool)); |
| |
| /* read contents of the current pack file */ |
| SVN_ERR(read_pack_revprop(&revprops, fs, rev, generation, pool)); |
| |
| /* serialize the new revprops */ |
| serialized = svn_stringbuf_create_empty(pool); |
| stream = svn_stream_from_stringbuf(serialized, pool); |
| SVN_ERR(svn_hash_write2(proplist, stream, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_stream_close(stream)); |
| |
| /* calculate the size of the new data */ |
| changed_index = (int)(rev - revprops->start_revision); |
| new_total_size = revprops->total_size - revprops->serialized_size |
| + serialized->len |
| + (revprops->offsets->nelts + 2) * SVN_INT64_BUFFER_SIZE; |
| |
| APR_ARRAY_IDX(revprops->sizes, changed_index, apr_off_t) = serialized->len; |
| |
| /* can we put the new data into the same pack as the before? */ |
| if ( new_total_size < ffd->revprop_pack_size |
| || revprops->sizes->nelts == 1) |
| { |
| /* simply replace the old pack file with new content as we do it |
| * in the non-packed case */ |
| |
| *final_path = svn_dirent_join(revprops->folder, revprops->filename, |
| pool); |
| SVN_ERR(svn_io_open_unique_file3(&file, tmp_path, revprops->folder, |
| svn_io_file_del_none, pool, pool)); |
| SVN_ERR(repack_revprops(fs, revprops, 0, revprops->sizes->nelts, |
| changed_index, serialized, new_total_size, |
| file, pool)); |
| } |
| else |
| { |
| /* split the pack file into two of roughly equal size */ |
| int right_count, left_count, i; |
| |
| int left = 0; |
| int right = revprops->sizes->nelts - 1; |
| apr_off_t left_size = 2 * SVN_INT64_BUFFER_SIZE; |
| apr_off_t right_size = 2 * SVN_INT64_BUFFER_SIZE; |
| |
| /* let left and right side grow such that their size difference |
| * is minimal after each step. */ |
| while (left <= right) |
| if ( left_size + APR_ARRAY_IDX(revprops->sizes, left, apr_off_t) |
| < right_size + APR_ARRAY_IDX(revprops->sizes, right, apr_off_t)) |
| { |
| left_size += APR_ARRAY_IDX(revprops->sizes, left, apr_off_t) |
| + SVN_INT64_BUFFER_SIZE; |
| ++left; |
| } |
| else |
| { |
| right_size += APR_ARRAY_IDX(revprops->sizes, right, apr_off_t) |
| + SVN_INT64_BUFFER_SIZE; |
| --right; |
| } |
| |
| /* since the items need much less than SVN_INT64_BUFFER_SIZE |
| * bytes to represent their length, the split may not be optimal */ |
| left_count = left; |
| right_count = revprops->sizes->nelts - left; |
| |
| /* if new_size is large, one side may exceed the pack size limit. |
| * In that case, split before and after the modified revprop.*/ |
| if ( left_size > ffd->revprop_pack_size |
| || right_size > ffd->revprop_pack_size) |
| { |
| left_count = changed_index; |
| right_count = revprops->sizes->nelts - left_count - 1; |
| } |
| |
| /* write the new, split files */ |
| if (left_count) |
| { |
| SVN_ERR(repack_file_open(&file, fs, revprops, 0, |
| left_count, files_to_delete, pool)); |
| SVN_ERR(repack_revprops(fs, revprops, 0, left_count, |
| changed_index, serialized, new_total_size, |
| file, pool)); |
| } |
| |
| if (left_count + right_count < revprops->sizes->nelts) |
| { |
| SVN_ERR(repack_file_open(&file, fs, revprops, changed_index, |
| changed_index + 1, files_to_delete, |
| pool)); |
| SVN_ERR(repack_revprops(fs, revprops, changed_index, |
| changed_index + 1, |
| changed_index, serialized, new_total_size, |
| file, pool)); |
| } |
| |
| if (right_count) |
| { |
| SVN_ERR(repack_file_open(&file, fs, revprops, |
| revprops->sizes->nelts - right_count, |
| revprops->sizes->nelts, |
| files_to_delete, pool)); |
| SVN_ERR(repack_revprops(fs, revprops, |
| revprops->sizes->nelts - right_count, |
| revprops->sizes->nelts, changed_index, |
| serialized, new_total_size, file, |
| pool)); |
| } |
| |
| /* write the new manifest */ |
| *final_path = svn_dirent_join(revprops->folder, PATH_MANIFEST, pool); |
| SVN_ERR(svn_io_open_unique_file3(&file, tmp_path, revprops->folder, |
| svn_io_file_del_none, pool, pool)); |
| |
| for (i = 0; i < revprops->manifest->nelts; ++i) |
| { |
| const char *filename = APR_ARRAY_IDX(revprops->manifest, i, |
| const char*); |
| SVN_ERR(svn_io_file_write_full(file, filename, strlen(filename), |
| NULL, pool)); |
| SVN_ERR(svn_io_file_putc('\n', file, pool)); |
| } |
| |
| SVN_ERR(svn_io_file_flush_to_disk(file, pool)); |
| SVN_ERR(svn_io_file_close(file, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Set the revision property list of revision REV in filesystem FS to |
| PROPLIST. Use POOL for temporary allocations. */ |
| static svn_error_t * |
| set_revision_proplist(svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_hash_t *proplist, |
| apr_pool_t *pool) |
| { |
| svn_boolean_t is_packed; |
| svn_boolean_t bump_generation = FALSE; |
| const char *final_path; |
| const char *tmp_path; |
| const char *perms_reference; |
| apr_array_header_t *files_to_delete = NULL; |
| |
| SVN_ERR(ensure_revision_exists(fs, rev, pool)); |
| |
| /* this info will not change while we hold the global FS write lock */ |
| is_packed = is_packed_revprop(fs, rev); |
| |
| /* Test whether revprops already exist for this revision. |
| * Only then will we need to bump the revprop generation. */ |
| if (has_revprop_cache(fs, pool)) |
| { |
| if (is_packed) |
| { |
| bump_generation = TRUE; |
| } |
| else |
| { |
| svn_node_kind_t kind; |
| SVN_ERR(svn_io_check_path(path_revprops(fs, rev, pool), &kind, |
| pool)); |
| bump_generation = kind != svn_node_none; |
| } |
| } |
| |
| /* Serialize the new revprop data */ |
| if (is_packed) |
| SVN_ERR(write_packed_revprop(&final_path, &tmp_path, &files_to_delete, |
| fs, rev, proplist, pool)); |
| else |
| SVN_ERR(write_non_packed_revprop(&final_path, &tmp_path, |
| fs, rev, proplist, pool)); |
| |
| /* We use the rev file of this revision as the perms reference, |
| * because when setting revprops for the first time, the revprop |
| * file won't exist and therefore can't serve as its own reference. |
| * (Whereas the rev file should already exist at this point.) |
| */ |
| SVN_ERR(svn_fs_fs__path_rev_absolute(&perms_reference, fs, rev, pool)); |
| |
| /* Now, switch to the new revprop data. */ |
| SVN_ERR(switch_to_new_revprop(fs, final_path, tmp_path, perms_reference, |
| files_to_delete, bump_generation, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__revision_proplist(apr_hash_t **proplist_p, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| SVN_ERR(get_revision_proplist(proplist_p, fs, rev, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Represents where in the current svndiff data block each |
| representation is. */ |
| struct rep_state |
| { |
| apr_file_t *file; |
| /* The txdelta window cache to use or NULL. */ |
| svn_cache__t *window_cache; |
| /* Caches un-deltified windows. May be NULL. */ |
| svn_cache__t *combined_cache; |
| apr_off_t start; /* The starting offset for the raw |
| svndiff/plaintext data minus header. */ |
| apr_off_t off; /* The current offset into the file. */ |
| apr_off_t end; /* The end offset of the raw data. */ |
| int ver; /* If a delta, what svndiff version? */ |
| int chunk_index; |
| }; |
| |
| /* See create_rep_state, which wraps this and adds another error. */ |
| static svn_error_t * |
| create_rep_state_body(struct rep_state **rep_state, |
| struct rep_args **rep_args, |
| apr_file_t **file_hint, |
| svn_revnum_t *rev_hint, |
| representation_t *rep, |
| svn_fs_t *fs, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| struct rep_state *rs = apr_pcalloc(pool, sizeof(*rs)); |
| struct rep_args *ra; |
| unsigned char buf[4]; |
| |
| /* If the hint is |
| * - given, |
| * - refers to a valid revision, |
| * - refers to a packed revision, |
| * - as does the rep we want to read, and |
| * - refers to the same pack file as the rep |
| * ... |
| */ |
| if ( file_hint && rev_hint && *file_hint |
| && SVN_IS_VALID_REVNUM(*rev_hint) |
| && *rev_hint < ffd->min_unpacked_rev |
| && rep->revision < ffd->min_unpacked_rev |
| && ( (*rev_hint / ffd->max_files_per_dir) |
| == (rep->revision / ffd->max_files_per_dir))) |
| { |
| /* ... we can re-use the same, already open file object |
| */ |
| apr_off_t offset; |
| SVN_ERR(get_packed_offset(&offset, fs, rep->revision, pool)); |
| |
| offset += rep->offset; |
| SVN_ERR(svn_io_file_seek(*file_hint, APR_SET, &offset, pool)); |
| |
| rs->file = *file_hint; |
| } |
| else |
| { |
| /* otherwise, create a new file object |
| */ |
| SVN_ERR(open_and_seek_representation(&rs->file, fs, rep, pool)); |
| } |
| |
| /* remember the current file, if suggested by the caller */ |
| if (file_hint) |
| *file_hint = rs->file; |
| if (rev_hint) |
| *rev_hint = rep->revision; |
| |
| /* continue constructing RS and RA */ |
| rs->window_cache = ffd->txdelta_window_cache; |
| rs->combined_cache = ffd->combined_window_cache; |
| |
| SVN_ERR(read_rep_line(&ra, rs->file, pool)); |
| SVN_ERR(get_file_offset(&rs->start, rs->file, pool)); |
| rs->off = rs->start; |
| rs->end = rs->start + rep->size; |
| *rep_state = rs; |
| *rep_args = ra; |
| |
| if (!ra->is_delta) |
| /* This is a plaintext, so just return the current rep_state. */ |
| return SVN_NO_ERROR; |
| |
| /* We are dealing with a delta, find out what version. */ |
| SVN_ERR(svn_io_file_read_full2(rs->file, buf, sizeof(buf), |
| NULL, NULL, pool)); |
| /* ### Layering violation */ |
| if (! ((buf[0] == 'S') && (buf[1] == 'V') && (buf[2] == 'N'))) |
| return svn_error_create |
| (SVN_ERR_FS_CORRUPT, NULL, |
| _("Malformed svndiff data in representation")); |
| rs->ver = buf[3]; |
| rs->chunk_index = 0; |
| rs->off += 4; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Read the rep args for REP in filesystem FS and create a rep_state |
| for reading the representation. Return the rep_state in *REP_STATE |
| and the rep args in *REP_ARGS, both allocated in POOL. |
| |
| When reading multiple reps, i.e. a skip delta chain, you may provide |
| non-NULL FILE_HINT and REV_HINT. (If FILE_HINT is not NULL, in the first |
| call it should be a pointer to NULL.) The function will use these variables |
| to store the previous call results and tries to re-use them. This may |
| result in significant savings in I/O for packed files. |
| */ |
| static svn_error_t * |
| create_rep_state(struct rep_state **rep_state, |
| struct rep_args **rep_args, |
| apr_file_t **file_hint, |
| svn_revnum_t *rev_hint, |
| representation_t *rep, |
| svn_fs_t *fs, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err = create_rep_state_body(rep_state, rep_args, |
| file_hint, rev_hint, |
| rep, fs, pool); |
| if (err && err->apr_err == SVN_ERR_FS_CORRUPT) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* ### This always returns "-1" for transaction reps, because |
| ### this particular bit of code doesn't know if the rep is |
| ### stored in the protorev or in the mutable area (for props |
| ### or dir contents). It is pretty rare for FSFS to *read* |
| ### from the protorev file, though, so this is probably OK. |
| ### And anyone going to debug corruption errors is probably |
| ### going to jump straight to this comment anyway! */ |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, err, |
| "Corrupt representation '%s'", |
| rep |
| ? representation_string(rep, ffd->format, TRUE, |
| TRUE, pool) |
| : "(null)"); |
| } |
| /* ### Call representation_string() ? */ |
| return svn_error_trace(err); |
| } |
| |
| struct rep_read_baton |
| { |
| /* The FS from which we're reading. */ |
| svn_fs_t *fs; |
| |
| /* If not NULL, this is the base for the first delta window in rs_list */ |
| svn_stringbuf_t *base_window; |
| |
| /* The state of all prior delta representations. */ |
| apr_array_header_t *rs_list; |
| |
| /* The plaintext state, if there is a plaintext. */ |
| struct rep_state *src_state; |
| |
| /* The index of the current delta chunk, if we are reading a delta. */ |
| int chunk_index; |
| |
| /* The buffer where we store undeltified data. */ |
| char *buf; |
| apr_size_t buf_pos; |
| apr_size_t buf_len; |
| |
| /* A checksum context for summing the data read in order to verify it. |
| Note: we don't need to use the sha1 checksum because we're only doing |
| data verification, for which md5 is perfectly safe. */ |
| svn_checksum_ctx_t *md5_checksum_ctx; |
| |
| svn_boolean_t checksum_finalized; |
| |
| /* The stored checksum of the representation we are reading, its |
| length, and the amount we've read so far. Some of this |
| information is redundant with rs_list and src_state, but it's |
| convenient for the checksumming code to have it here. */ |
| svn_checksum_t *md5_checksum; |
| |
| svn_filesize_t len; |
| svn_filesize_t off; |
| |
| /* The key for the fulltext cache for this rep, if there is a |
| fulltext cache. */ |
| pair_cache_key_t fulltext_cache_key; |
| /* The text we've been reading, if we're going to cache it. */ |
| svn_stringbuf_t *current_fulltext; |
| |
| /* Used for temporary allocations during the read. */ |
| apr_pool_t *pool; |
| |
| /* Pool used to store file handles and other data that is persistant |
| for the entire stream read. */ |
| apr_pool_t *filehandle_pool; |
| }; |
| |
| /* Combine the name of the rev file in RS with the given OFFSET to form |
| * a cache lookup key. Allocations will be made from POOL. May return |
| * NULL if the key cannot be constructed. */ |
| static const char* |
| get_window_key(struct rep_state *rs, apr_off_t offset, apr_pool_t *pool) |
| { |
| const char *name; |
| const char *last_part; |
| const char *name_last; |
| |
| /* the rev file name containing the txdelta window. |
| * If this fails we are in serious trouble anyways. |
| * And if nobody else detects the problems, the file content checksum |
| * comparison _will_ find them. |
| */ |
| if (apr_file_name_get(&name, rs->file)) |
| return NULL; |
| |
| /* Handle packed files as well by scanning backwards until we find the |
| * revision or pack number. */ |
| name_last = name + strlen(name) - 1; |
| while (! svn_ctype_isdigit(*name_last)) |
| --name_last; |
| |
| last_part = name_last; |
| while (svn_ctype_isdigit(*last_part)) |
| --last_part; |
| |
| /* We must differentiate between packed files (as of today, the number |
| * is being followed by a dot) and non-packed files (followed by \0). |
| * Otherwise, there might be overlaps in the numbering range if the |
| * repo gets packed after caching the txdeltas of non-packed revs. |
| * => add the first non-digit char to the packed number. */ |
| if (name_last[1] != '\0') |
| ++name_last; |
| |
| /* copy one char MORE than the actual number to mark packed files, |
| * i.e. packed revision file content uses different key space then |
| * non-packed ones: keys for packed rev file content ends with a dot |
| * for non-packed rev files they end with a digit. */ |
| name = apr_pstrndup(pool, last_part + 1, name_last - last_part); |
| return svn_fs_fs__combine_number_and_string(offset, name, pool); |
| } |
| |
| /* Read the WINDOW_P for the rep state RS from the current FSFS session's |
| * cache. This will be a no-op and IS_CACHED will be set to FALSE if no |
| * cache has been given. If a cache is available IS_CACHED will inform |
| * the caller about the success of the lookup. Allocations (of the window |
| * in particualar) will be made from POOL. |
| * |
| * If the information could be found, put RS and the position within the |
| * rev file into the same state as if the data had just been read from it. |
| */ |
| static svn_error_t * |
| get_cached_window(svn_txdelta_window_t **window_p, |
| struct rep_state *rs, |
| svn_boolean_t *is_cached, |
| apr_pool_t *pool) |
| { |
| if (! rs->window_cache) |
| { |
| /* txdelta window has not been enabled */ |
| *is_cached = FALSE; |
| } |
| else |
| { |
| /* ask the cache for the desired txdelta window */ |
| svn_fs_fs__txdelta_cached_window_t *cached_window; |
| SVN_ERR(svn_cache__get((void **) &cached_window, |
| is_cached, |
| rs->window_cache, |
| get_window_key(rs, rs->off, pool), |
| pool)); |
| |
| if (*is_cached) |
| { |
| /* found it. Pass it back to the caller. */ |
| *window_p = cached_window->window; |
| |
| /* manipulate the RS as if we just read the data */ |
| rs->chunk_index++; |
| rs->off = cached_window->end_offset; |
| |
| /* manipulate the rev file as if we just read from it */ |
| SVN_ERR(svn_io_file_seek(rs->file, APR_SET, &rs->off, pool)); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Store the WINDOW read at OFFSET for the rep state RS in the current |
| * FSFS session's cache. This will be a no-op if no cache has been given. |
| * Temporary allocations will be made from SCRATCH_POOL. */ |
| static svn_error_t * |
| set_cached_window(svn_txdelta_window_t *window, |
| struct rep_state *rs, |
| apr_off_t offset, |
| apr_pool_t *scratch_pool) |
| { |
| if (rs->window_cache) |
| { |
| /* store the window and the first offset _past_ it */ |
| svn_fs_fs__txdelta_cached_window_t cached_window; |
| |
| cached_window.window = window; |
| cached_window.end_offset = rs->off; |
| |
| /* but key it with the start offset because that is the known state |
| * when we will look it up */ |
| return svn_cache__set(rs->window_cache, |
| get_window_key(rs, offset, scratch_pool), |
| &cached_window, |
| scratch_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Read the WINDOW_P for the rep state RS from the current FSFS session's |
| * cache. This will be a no-op and IS_CACHED will be set to FALSE if no |
| * cache has been given. If a cache is available IS_CACHED will inform |
| * the caller about the success of the lookup. Allocations (of the window |
| * in particualar) will be made from POOL. |
| */ |
| static svn_error_t * |
| get_cached_combined_window(svn_stringbuf_t **window_p, |
| struct rep_state *rs, |
| svn_boolean_t *is_cached, |
| apr_pool_t *pool) |
| { |
| if (! rs->combined_cache) |
| { |
| /* txdelta window has not been enabled */ |
| *is_cached = FALSE; |
| } |
| else |
| { |
| /* ask the cache for the desired txdelta window */ |
| return svn_cache__get((void **)window_p, |
| is_cached, |
| rs->combined_cache, |
| get_window_key(rs, rs->start, pool), |
| pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Store the WINDOW read at OFFSET for the rep state RS in the current |
| * FSFS session's cache. This will be a no-op if no cache has been given. |
| * Temporary allocations will be made from SCRATCH_POOL. */ |
| static svn_error_t * |
| set_cached_combined_window(svn_stringbuf_t *window, |
| struct rep_state *rs, |
| apr_off_t offset, |
| apr_pool_t *scratch_pool) |
| { |
| if (rs->combined_cache) |
| { |
| /* but key it with the start offset because that is the known state |
| * when we will look it up */ |
| return svn_cache__set(rs->combined_cache, |
| get_window_key(rs, offset, scratch_pool), |
| window, |
| scratch_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Build an array of rep_state structures in *LIST giving the delta |
| reps from first_rep to a plain-text or self-compressed rep. Set |
| *SRC_STATE to the plain-text rep we find at the end of the chain, |
| or to NULL if the final delta representation is self-compressed. |
| The representation to start from is designated by filesystem FS, id |
| ID, and representation REP. |
| Also, set *WINDOW_P to the base window content for *LIST, if it |
| could be found in cache. Otherwise, *LIST will contain the base |
| representation for the whole delta chain. |
| Finally, return the expanded size of the representation in |
| *EXPANDED_SIZE. It will take care of cases where only the on-disk |
| size is known. */ |
| static svn_error_t * |
| build_rep_list(apr_array_header_t **list, |
| svn_stringbuf_t **window_p, |
| struct rep_state **src_state, |
| svn_filesize_t *expanded_size, |
| svn_fs_t *fs, |
| representation_t *first_rep, |
| apr_pool_t *pool) |
| { |
| representation_t rep; |
| struct rep_state *rs = NULL; |
| struct rep_args *rep_args; |
| svn_boolean_t is_cached = FALSE; |
| apr_file_t *last_file = NULL; |
| svn_revnum_t last_revision; |
| |
| *list = apr_array_make(pool, 1, sizeof(struct rep_state *)); |
| rep = *first_rep; |
| |
| /* The value as stored in the data struct. |
| 0 is either for unknown length or actually zero length. */ |
| *expanded_size = first_rep->expanded_size; |
| |
| /* for the top-level rep, we need the rep_args */ |
| SVN_ERR(create_rep_state(&rs, &rep_args, &last_file, |
| &last_revision, &rep, fs, pool)); |
| |
| /* Unknown size or empty representation? |
| That implies the this being the first iteration. |
| Usually size equals on-disk size, except for empty, |
| compressed representations (delta, size = 4). |
| Please note that for all non-empty deltas have |
| a 4-byte header _plus_ some data. */ |
| if (*expanded_size == 0) |
| if (! rep_args->is_delta || first_rep->size != 4) |
| *expanded_size = first_rep->size; |
| |
| while (1) |
| { |
| /* fetch state, if that has not been done already */ |
| if (!rs) |
| SVN_ERR(create_rep_state(&rs, &rep_args, &last_file, |
| &last_revision, &rep, fs, pool)); |
| |
| SVN_ERR(get_cached_combined_window(window_p, rs, &is_cached, pool)); |
| if (is_cached) |
| { |
| /* We already have a reconstructed window in our cache. |
| Write a pseudo rep_state with the full length. */ |
| rs->off = rs->start; |
| rs->end = rs->start + (*window_p)->len; |
| *src_state = rs; |
| return SVN_NO_ERROR; |
| } |
| |
| if (!rep_args->is_delta) |
| { |
| /* This is a plaintext, so just return the current rep_state. */ |
| *src_state = rs; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Push this rep onto the list. If it's self-compressed, we're done. */ |
| APR_ARRAY_PUSH(*list, struct rep_state *) = rs; |
| if (rep_args->is_delta_vs_empty) |
| { |
| *src_state = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| rep.revision = rep_args->base_revision; |
| rep.offset = rep_args->base_offset; |
| rep.size = rep_args->base_length; |
| rep.txn_id = NULL; |
| |
| rs = NULL; |
| } |
| } |
| |
| /* Determine the optimal size of a string buf that shall receive a |
| * (full-) text of NEEDED bytes. |
| * |
| * The critical point is that those buffers may be very large and |
| * can cause memory fragmentation. We apply simple heuristics to |
| * make fragmentation less likely. |
| */ |
| static apr_size_t |
| optimimal_allocation_size(apr_size_t needed) |
| { |
| /* For all allocations, assume some overhead that is shared between |
| * OS memory managemnt, APR memory management and svn_stringbuf_t. */ |
| const apr_size_t overhead = 0x400; |
| apr_size_t optimal; |
| |
| /* If an allocation size if safe for other ephemeral buffers, it should |
| * be safe for ours. */ |
| if (needed <= SVN__STREAM_CHUNK_SIZE) |
| return needed; |
| |
| /* Paranoia edge case: |
| * Skip our heuristics if they created arithmetical overflow. |
| * Beware to make this test work for NEEDED = APR_SIZE_MAX as well! */ |
| if (needed >= APR_SIZE_MAX / 2 - overhead) |
| return needed; |
| |
| /* As per definition SVN__STREAM_CHUNK_SIZE is a power of two. |
| * Since we know NEEDED to be larger than that, use it as the |
| * starting point. |
| * |
| * Heuristics: Allocate a power-of-two number of bytes that fit |
| * NEEDED plus some OVERHEAD. The APR allocator |
| * will round it up to the next full page size. |
| */ |
| optimal = SVN__STREAM_CHUNK_SIZE; |
| while (optimal - overhead < needed) |
| optimal *= 2; |
| |
| /* This is above or equal to NEEDED. */ |
| return optimal - overhead; |
| } |
| |
| |
| |
| /* Create a rep_read_baton structure for node revision NODEREV in |
| filesystem FS and store it in *RB_P. If FULLTEXT_CACHE_KEY is not |
| NULL, it is the rep's key in the fulltext cache, and a stringbuf |
| must be allocated to store the text. Perform all allocations in |
| POOL. If rep is mutable, it must be for file contents. */ |
| static svn_error_t * |
| rep_read_get_baton(struct rep_read_baton **rb_p, |
| svn_fs_t *fs, |
| representation_t *rep, |
| pair_cache_key_t fulltext_cache_key, |
| apr_pool_t *pool) |
| { |
| struct rep_read_baton *b; |
| |
| b = apr_pcalloc(pool, sizeof(*b)); |
| b->fs = fs; |
| b->base_window = NULL; |
| b->chunk_index = 0; |
| b->buf = NULL; |
| b->md5_checksum_ctx = svn_checksum_ctx_create(svn_checksum_md5, pool); |
| b->checksum_finalized = FALSE; |
| b->md5_checksum = svn_checksum_dup(rep->md5_checksum, pool); |
| b->len = rep->expanded_size; |
| b->off = 0; |
| b->fulltext_cache_key = fulltext_cache_key; |
| b->pool = svn_pool_create(pool); |
| b->filehandle_pool = svn_pool_create(pool); |
| |
| SVN_ERR(build_rep_list(&b->rs_list, &b->base_window, |
| &b->src_state, &b->len, fs, rep, |
| b->filehandle_pool)); |
| |
| if (SVN_IS_VALID_REVNUM(fulltext_cache_key.revision)) |
| b->current_fulltext = svn_stringbuf_create_ensure |
| (optimimal_allocation_size((apr_size_t)b->len), |
| b->filehandle_pool); |
| else |
| b->current_fulltext = NULL; |
| |
| /* Save our output baton. */ |
| *rb_p = b; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Skip forwards to THIS_CHUNK in REP_STATE and then read the next delta |
| window into *NWIN. */ |
| static svn_error_t * |
| read_delta_window(svn_txdelta_window_t **nwin, int this_chunk, |
| struct rep_state *rs, apr_pool_t *pool) |
| { |
| svn_stream_t *stream; |
| svn_boolean_t is_cached; |
| apr_off_t old_offset; |
| |
| SVN_ERR_ASSERT(rs->chunk_index <= this_chunk); |
| |
| /* RS->FILE may be shared between RS instances -> make sure we point |
| * to the right data. */ |
| SVN_ERR(svn_io_file_seek(rs->file, APR_SET, &rs->off, pool)); |
| |
| /* Skip windows to reach the current chunk if we aren't there yet. */ |
| while (rs->chunk_index < this_chunk) |
| { |
| SVN_ERR(svn_txdelta_skip_svndiff_window(rs->file, rs->ver, pool)); |
| rs->chunk_index++; |
| SVN_ERR(get_file_offset(&rs->off, rs->file, pool)); |
| if (rs->off >= rs->end) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Reading one svndiff window read " |
| "beyond the end of the " |
| "representation")); |
| } |
| |
| /* Read the next window. But first, try to find it in the cache. */ |
| SVN_ERR(get_cached_window(nwin, rs, &is_cached, pool)); |
| if (is_cached) |
| return SVN_NO_ERROR; |
| |
| /* Actually read the next window. */ |
| old_offset = rs->off; |
| stream = svn_stream_from_aprfile2(rs->file, TRUE, pool); |
| SVN_ERR(svn_txdelta_read_svndiff_window(nwin, stream, rs->ver, pool)); |
| rs->chunk_index++; |
| SVN_ERR(get_file_offset(&rs->off, rs->file, pool)); |
| |
| if (rs->off > rs->end) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Reading one svndiff window read beyond " |
| "the end of the representation")); |
| |
| /* the window has not been cached before, thus cache it now |
| * (if caching is used for them at all) */ |
| return set_cached_window(*nwin, rs, old_offset, pool); |
| } |
| |
| /* Read SIZE bytes from the representation RS and return it in *NWIN. */ |
| static svn_error_t * |
| read_plain_window(svn_stringbuf_t **nwin, struct rep_state *rs, |
| apr_size_t size, apr_pool_t *pool) |
| { |
| /* RS->FILE may be shared between RS instances -> make sure we point |
| * to the right data. */ |
| SVN_ERR(svn_io_file_seek(rs->file, APR_SET, &rs->off, pool)); |
| |
| /* Read the plain data. */ |
| *nwin = svn_stringbuf_create_ensure(size, pool); |
| SVN_ERR(svn_io_file_read_full2(rs->file, (*nwin)->data, size, NULL, NULL, |
| pool)); |
| (*nwin)->data[size] = 0; |
| |
| /* Update RS. */ |
| rs->off += (apr_off_t)size; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Get the undeltified window that is a result of combining all deltas |
| from the current desired representation identified in *RB with its |
| base representation. Store the window in *RESULT. */ |
| static svn_error_t * |
| get_combined_window(svn_stringbuf_t **result, |
| struct rep_read_baton *rb) |
| { |
| apr_pool_t *pool, *new_pool, *window_pool; |
| int i; |
| svn_txdelta_window_t *window; |
| apr_array_header_t *windows; |
| svn_stringbuf_t *source, *buf = rb->base_window; |
| struct rep_state *rs; |
| |
| /* Read all windows that we need to combine. This is fine because |
| the size of each window is relatively small (100kB) and skip- |
| delta limits the number of deltas in a chain to well under 100. |
| Stop early if one of them does not depend on its predecessors. */ |
| window_pool = svn_pool_create(rb->pool); |
| windows = apr_array_make(window_pool, 0, sizeof(svn_txdelta_window_t *)); |
| for (i = 0; i < rb->rs_list->nelts; ++i) |
| { |
| rs = APR_ARRAY_IDX(rb->rs_list, i, struct rep_state *); |
| SVN_ERR(read_delta_window(&window, rb->chunk_index, rs, window_pool)); |
| |
| APR_ARRAY_PUSH(windows, svn_txdelta_window_t *) = window; |
| if (window->src_ops == 0) |
| { |
| ++i; |
| break; |
| } |
| } |
| |
| /* Combine in the windows from the other delta reps. */ |
| pool = svn_pool_create(rb->pool); |
| for (--i; i >= 0; --i) |
| { |
| |
| rs = APR_ARRAY_IDX(rb->rs_list, i, struct rep_state *); |
| window = APR_ARRAY_IDX(windows, i, svn_txdelta_window_t *); |
| |
| /* Maybe, we've got a PLAIN start representation. If we do, read |
| as much data from it as the needed for the txdelta window's source |
| view. |
| Note that BUF / SOURCE may only be NULL in the first iteration. |
| Also note that we may have short-cut reading the delta chain -- |
| in which case SRC_OPS is 0 and it might not be a PLAIN rep. */ |
| source = buf; |
| if (source == NULL && rb->src_state != NULL && window->src_ops) |
| SVN_ERR(read_plain_window(&source, rb->src_state, window->sview_len, |
| pool)); |
| |
| /* Combine this window with the current one. */ |
| new_pool = svn_pool_create(rb->pool); |
| buf = svn_stringbuf_create_ensure(window->tview_len, new_pool); |
| buf->len = window->tview_len; |
| |
| svn_txdelta_apply_instructions(window, source ? source->data : NULL, |
| buf->data, &buf->len); |
| if (buf->len != window->tview_len) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("svndiff window length is " |
| "corrupt")); |
| |
| /* Cache windows only if the whole rep content could be read as a |
| single chunk. Only then will no other chunk need a deeper RS |
| list than the cached chunk. */ |
| if ((rb->chunk_index == 0) && (rs->off == rs->end)) |
| SVN_ERR(set_cached_combined_window(buf, rs, rs->start, new_pool)); |
| |
| /* Cycle pools so that we only need to hold three windows at a time. */ |
| svn_pool_destroy(pool); |
| pool = new_pool; |
| } |
| |
| svn_pool_destroy(window_pool); |
| |
| *result = buf; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Returns whether or not the expanded fulltext of the file is cachable |
| * based on its size SIZE. The decision depends on the cache used by RB. |
| */ |
| static svn_boolean_t |
| fulltext_size_is_cachable(fs_fs_data_t *ffd, svn_filesize_t size) |
| { |
| return (size < APR_SIZE_MAX) |
| && svn_cache__is_cachable(ffd->fulltext_cache, (apr_size_t)size); |
| } |
| |
| /* Close method used on streams returned by read_representation(). |
| */ |
| static svn_error_t * |
| rep_read_contents_close(void *baton) |
| { |
| struct rep_read_baton *rb = baton; |
| |
| svn_pool_destroy(rb->pool); |
| svn_pool_destroy(rb->filehandle_pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return the next *LEN bytes of the rep and store them in *BUF. */ |
| static svn_error_t * |
| get_contents(struct rep_read_baton *rb, |
| char *buf, |
| apr_size_t *len) |
| { |
| apr_size_t copy_len, remaining = *len; |
| char *cur = buf; |
| struct rep_state *rs; |
| |
| /* Special case for when there are no delta reps, only a plain |
| text. */ |
| if (rb->rs_list->nelts == 0) |
| { |
| copy_len = remaining; |
| rs = rb->src_state; |
| |
| if (rb->base_window != NULL) |
| { |
| /* We got the desired rep directly from the cache. |
| This is where we need the pseudo rep_state created |
| by build_rep_list(). */ |
| apr_size_t offset = (apr_size_t)(rs->off - rs->start); |
| if (copy_len + offset > rb->base_window->len) |
| copy_len = offset < rb->base_window->len |
| ? rb->base_window->len - offset |
| : 0ul; |
| |
| memcpy (cur, rb->base_window->data + offset, copy_len); |
| } |
| else |
| { |
| if (((apr_off_t) copy_len) > rs->end - rs->off) |
| copy_len = (apr_size_t) (rs->end - rs->off); |
| SVN_ERR(svn_io_file_read_full2(rs->file, cur, copy_len, NULL, |
| NULL, rb->pool)); |
| } |
| |
| rs->off += copy_len; |
| *len = copy_len; |
| return SVN_NO_ERROR; |
| } |
| |
| while (remaining > 0) |
| { |
| /* If we have buffered data from a previous chunk, use that. */ |
| if (rb->buf) |
| { |
| /* Determine how much to copy from the buffer. */ |
| copy_len = rb->buf_len - rb->buf_pos; |
| if (copy_len > remaining) |
| copy_len = remaining; |
| |
| /* Actually copy the data. */ |
| memcpy(cur, rb->buf + rb->buf_pos, copy_len); |
| rb->buf_pos += copy_len; |
| cur += copy_len; |
| remaining -= copy_len; |
| |
| /* If the buffer is all used up, clear it and empty the |
| local pool. */ |
| if (rb->buf_pos == rb->buf_len) |
| { |
| svn_pool_clear(rb->pool); |
| rb->buf = NULL; |
| } |
| } |
| else |
| { |
| svn_stringbuf_t *sbuf = NULL; |
| |
| rs = APR_ARRAY_IDX(rb->rs_list, 0, struct rep_state *); |
| if (rs->off == rs->end) |
| break; |
| |
| /* Get more buffered data by evaluating a chunk. */ |
| SVN_ERR(get_combined_window(&sbuf, rb)); |
| |
| rb->chunk_index++; |
| rb->buf_len = sbuf->len; |
| rb->buf = sbuf->data; |
| rb->buf_pos = 0; |
| } |
| } |
| |
| *len = cur - buf; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* BATON is of type `rep_read_baton'; read the next *LEN bytes of the |
| representation and store them in *BUF. Sum as we read and verify |
| the MD5 sum at the end. */ |
| static svn_error_t * |
| rep_read_contents(void *baton, |
| char *buf, |
| apr_size_t *len) |
| { |
| struct rep_read_baton *rb = baton; |
| |
| /* Get the next block of data. */ |
| SVN_ERR(get_contents(rb, buf, len)); |
| |
| if (rb->current_fulltext) |
| svn_stringbuf_appendbytes(rb->current_fulltext, buf, *len); |
| |
| /* Perform checksumming. We want to check the checksum as soon as |
| the last byte of data is read, in case the caller never performs |
| a short read, but we don't want to finalize the MD5 context |
| twice. */ |
| if (!rb->checksum_finalized) |
| { |
| SVN_ERR(svn_checksum_update(rb->md5_checksum_ctx, buf, *len)); |
| rb->off += *len; |
| if (rb->off == rb->len) |
| { |
| svn_checksum_t *md5_checksum; |
| |
| rb->checksum_finalized = TRUE; |
| SVN_ERR(svn_checksum_final(&md5_checksum, rb->md5_checksum_ctx, |
| rb->pool)); |
| if (!svn_checksum_match(md5_checksum, rb->md5_checksum)) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, |
| svn_checksum_mismatch_err(rb->md5_checksum, md5_checksum, |
| rb->pool, |
| _("Checksum mismatch while reading representation")), |
| NULL); |
| } |
| } |
| |
| if (rb->off == rb->len && rb->current_fulltext) |
| { |
| fs_fs_data_t *ffd = rb->fs->fsap_data; |
| SVN_ERR(svn_cache__set(ffd->fulltext_cache, &rb->fulltext_cache_key, |
| rb->current_fulltext, rb->pool)); |
| rb->current_fulltext = NULL; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Return a stream in *CONTENTS_P that will read the contents of a |
| representation stored at the location given by REP. Appropriate |
| for any kind of immutable representation, but only for file |
| contents (not props or directory contents) in mutable |
| representations. |
| |
| If REP is NULL, the representation is assumed to be empty, and the |
| empty stream is returned. |
| */ |
| static svn_error_t * |
| read_representation(svn_stream_t **contents_p, |
| svn_fs_t *fs, |
| representation_t *rep, |
| apr_pool_t *pool) |
| { |
| if (! rep) |
| { |
| *contents_p = svn_stream_empty(pool); |
| } |
| else |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| pair_cache_key_t fulltext_cache_key = { 0 }; |
| svn_filesize_t len = rep->expanded_size ? rep->expanded_size : rep->size; |
| struct rep_read_baton *rb; |
| |
| fulltext_cache_key.revision = rep->revision; |
| fulltext_cache_key.second = rep->offset; |
| if (ffd->fulltext_cache && SVN_IS_VALID_REVNUM(rep->revision) |
| && fulltext_size_is_cachable(ffd, len)) |
| { |
| svn_stringbuf_t *fulltext; |
| svn_boolean_t is_cached; |
| SVN_ERR(svn_cache__get((void **) &fulltext, &is_cached, |
| ffd->fulltext_cache, &fulltext_cache_key, |
| pool)); |
| if (is_cached) |
| { |
| *contents_p = svn_stream_from_stringbuf(fulltext, pool); |
| return SVN_NO_ERROR; |
| } |
| } |
| else |
| fulltext_cache_key.revision = SVN_INVALID_REVNUM; |
| |
| SVN_ERR(rep_read_get_baton(&rb, fs, rep, fulltext_cache_key, pool)); |
| |
| *contents_p = svn_stream_create(rb, pool); |
| svn_stream_set_read(*contents_p, rep_read_contents); |
| svn_stream_set_close(*contents_p, rep_read_contents_close); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__get_contents(svn_stream_t **contents_p, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_pool_t *pool) |
| { |
| return read_representation(contents_p, fs, noderev->data_rep, pool); |
| } |
| |
| /* Baton used when reading delta windows. */ |
| struct delta_read_baton |
| { |
| struct rep_state *rs; |
| svn_checksum_t *checksum; |
| }; |
| |
| /* This implements the svn_txdelta_next_window_fn_t interface. */ |
| static svn_error_t * |
| delta_read_next_window(svn_txdelta_window_t **window, void *baton, |
| apr_pool_t *pool) |
| { |
| struct delta_read_baton *drb = baton; |
| |
| if (drb->rs->off == drb->rs->end) |
| { |
| *window = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| return read_delta_window(window, drb->rs->chunk_index, drb->rs, pool); |
| } |
| |
| /* This implements the svn_txdelta_md5_digest_fn_t interface. */ |
| static const unsigned char * |
| delta_read_md5_digest(void *baton) |
| { |
| struct delta_read_baton *drb = baton; |
| |
| if (drb->checksum->kind == svn_checksum_md5) |
| return drb->checksum->digest; |
| else |
| return NULL; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__get_file_delta_stream(svn_txdelta_stream_t **stream_p, |
| svn_fs_t *fs, |
| node_revision_t *source, |
| node_revision_t *target, |
| apr_pool_t *pool) |
| { |
| svn_stream_t *source_stream, *target_stream; |
| |
| /* Try a shortcut: if the target is stored as a delta against the source, |
| then just use that delta. */ |
| if (source && source->data_rep && target->data_rep) |
| { |
| struct rep_state *rep_state; |
| struct rep_args *rep_args; |
| |
| /* Read target's base rep if any. */ |
| SVN_ERR(create_rep_state(&rep_state, &rep_args, NULL, NULL, |
| target->data_rep, fs, pool)); |
| |
| /* If that matches source, then use this delta as is. |
| Note that we want an actual delta here. E.g. a self-delta would |
| not be good enough. */ |
| if (rep_args->is_delta |
| && rep_args->base_revision == source->data_rep->revision |
| && rep_args->base_offset == source->data_rep->offset) |
| { |
| /* Create the delta read baton. */ |
| struct delta_read_baton *drb = apr_pcalloc(pool, sizeof(*drb)); |
| drb->rs = rep_state; |
| drb->checksum = svn_checksum_dup(target->data_rep->md5_checksum, |
| pool); |
| *stream_p = svn_txdelta_stream_create(drb, delta_read_next_window, |
| delta_read_md5_digest, pool); |
| return SVN_NO_ERROR; |
| } |
| else |
| SVN_ERR(svn_io_file_close(rep_state->file, pool)); |
| } |
| |
| /* Read both fulltexts and construct a delta. */ |
| if (source) |
| SVN_ERR(read_representation(&source_stream, fs, source->data_rep, pool)); |
| else |
| source_stream = svn_stream_empty(pool); |
| SVN_ERR(read_representation(&target_stream, fs, target->data_rep, pool)); |
| |
| /* Because source and target stream will already verify their content, |
| * there is no need to do this once more. In particular if the stream |
| * content is being fetched from cache. */ |
| svn_txdelta2(stream_p, source_stream, target_stream, FALSE, pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Baton for cache_access_wrapper. Wraps the original parameters of |
| * svn_fs_fs__try_process_file_content(). |
| */ |
| typedef struct cache_access_wrapper_baton_t |
| { |
| svn_fs_process_contents_func_t func; |
| void* baton; |
| } cache_access_wrapper_baton_t; |
| |
| /* Wrapper to translate between svn_fs_process_contents_func_t and |
| * svn_cache__partial_getter_func_t. |
| */ |
| static svn_error_t * |
| cache_access_wrapper(void **out, |
| const void *data, |
| apr_size_t data_len, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| cache_access_wrapper_baton_t *wrapper_baton = baton; |
| |
| SVN_ERR(wrapper_baton->func((const unsigned char *)data, |
| data_len - 1, /* cache adds terminating 0 */ |
| wrapper_baton->baton, |
| pool)); |
| |
| /* non-NULL value to signal the calling cache that all went well */ |
| *out = baton; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__try_process_file_contents(svn_boolean_t *success, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| svn_fs_process_contents_func_t processor, |
| void* baton, |
| apr_pool_t *pool) |
| { |
| representation_t *rep = noderev->data_rep; |
| if (rep) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| pair_cache_key_t fulltext_cache_key = { 0 }; |
| |
| fulltext_cache_key.revision = rep->revision; |
| fulltext_cache_key.second = rep->offset; |
| if (ffd->fulltext_cache && SVN_IS_VALID_REVNUM(rep->revision) |
| && fulltext_size_is_cachable(ffd, rep->expanded_size)) |
| { |
| cache_access_wrapper_baton_t wrapper_baton; |
| void *dummy = NULL; |
| |
| wrapper_baton.func = processor; |
| wrapper_baton.baton = baton; |
| return svn_cache__get_partial(&dummy, success, |
| ffd->fulltext_cache, |
| &fulltext_cache_key, |
| cache_access_wrapper, |
| &wrapper_baton, |
| pool); |
| } |
| } |
| |
| *success = FALSE; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Fetch the contents of a directory into ENTRIES. Values are stored |
| as filename to string mappings; further conversion is necessary to |
| convert them into svn_fs_dirent_t values. */ |
| static svn_error_t * |
| get_dir_contents(apr_hash_t *entries, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_pool_t *pool) |
| { |
| svn_stream_t *contents; |
| |
| if (noderev->data_rep && noderev->data_rep->txn_id) |
| { |
| const char *filename = path_txn_node_children(fs, noderev->id, pool); |
| |
| /* The representation is mutable. Read the old directory |
| contents from the mutable children file, followed by the |
| changes we've made in this transaction. */ |
| SVN_ERR(svn_stream_open_readonly(&contents, filename, pool, pool)); |
| SVN_ERR(svn_hash_read2(entries, contents, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_hash_read_incremental(entries, contents, NULL, pool)); |
| SVN_ERR(svn_stream_close(contents)); |
| } |
| else if (noderev->data_rep) |
| { |
| /* use a temporary pool for temp objects. |
| * Also undeltify content before parsing it. Otherwise, we could only |
| * parse it byte-by-byte. |
| */ |
| apr_pool_t *text_pool = svn_pool_create(pool); |
| apr_size_t len = noderev->data_rep->expanded_size |
| ? (apr_size_t)noderev->data_rep->expanded_size |
| : (apr_size_t)noderev->data_rep->size; |
| svn_stringbuf_t *text = svn_stringbuf_create_ensure(len, text_pool); |
| text->len = len; |
| |
| /* The representation is immutable. Read it normally. */ |
| SVN_ERR(read_representation(&contents, fs, noderev->data_rep, text_pool)); |
| SVN_ERR(svn_stream_read(contents, text->data, &text->len)); |
| SVN_ERR(svn_stream_close(contents)); |
| |
| /* de-serialize hash */ |
| contents = svn_stream_from_stringbuf(text, text_pool); |
| SVN_ERR(svn_hash_read2(entries, contents, SVN_HASH_TERMINATOR, pool)); |
| |
| svn_pool_destroy(text_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static const char * |
| unparse_dir_entry(svn_node_kind_t kind, const svn_fs_id_t *id, |
| apr_pool_t *pool) |
| { |
| return apr_psprintf(pool, "%s %s", |
| (kind == svn_node_file) ? KIND_FILE : KIND_DIR, |
| svn_fs_fs__id_unparse(id, pool)->data); |
| } |
| |
| /* Given a hash ENTRIES of dirent structions, return a hash in |
| *STR_ENTRIES_P, that has svn_string_t as the values in the format |
| specified by the fs_fs directory contents file. Perform |
| allocations in POOL. */ |
| static svn_error_t * |
| unparse_dir_entries(apr_hash_t **str_entries_p, |
| apr_hash_t *entries, |
| apr_pool_t *pool) |
| { |
| apr_hash_index_t *hi; |
| |
| /* For now, we use a our own hash function to ensure that we get a |
| * (largely) stable order when serializing the data. It also gives |
| * us some performance improvement. |
| * |
| * ### TODO ### |
| * Use some sorted or other fixed order data container. |
| */ |
| *str_entries_p = svn_hash__make(pool); |
| |
| for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) |
| { |
| const void *key; |
| apr_ssize_t klen; |
| svn_fs_dirent_t *dirent = svn__apr_hash_index_val(hi); |
| const char *new_val; |
| |
| apr_hash_this(hi, &key, &klen, NULL); |
| new_val = unparse_dir_entry(dirent->kind, dirent->id, pool); |
| apr_hash_set(*str_entries_p, key, klen, |
| svn_string_create(new_val, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Given a hash STR_ENTRIES with values as svn_string_t as specified |
| in an FSFS directory contents listing, return a hash of dirents in |
| *ENTRIES_P. Perform allocations in POOL. */ |
| static svn_error_t * |
| parse_dir_entries(apr_hash_t **entries_p, |
| apr_hash_t *str_entries, |
| const char *unparsed_id, |
| apr_pool_t *pool) |
| { |
| apr_hash_index_t *hi; |
| |
| *entries_p = apr_hash_make(pool); |
| |
| /* Translate the string dir entries into real entries. */ |
| for (hi = apr_hash_first(pool, str_entries); hi; hi = apr_hash_next(hi)) |
| { |
| const char *name = svn__apr_hash_index_key(hi); |
| svn_string_t *str_val = svn__apr_hash_index_val(hi); |
| char *str, *last_str; |
| svn_fs_dirent_t *dirent = apr_pcalloc(pool, sizeof(*dirent)); |
| |
| last_str = apr_pstrdup(pool, str_val->data); |
| dirent->name = apr_pstrdup(pool, name); |
| |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (str == NULL) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Directory entry corrupt in '%s'"), |
| unparsed_id); |
| |
| if (strcmp(str, KIND_FILE) == 0) |
| { |
| dirent->kind = svn_node_file; |
| } |
| else if (strcmp(str, KIND_DIR) == 0) |
| { |
| dirent->kind = svn_node_dir; |
| } |
| else |
| { |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Directory entry corrupt in '%s'"), |
| unparsed_id); |
| } |
| |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (str == NULL) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Directory entry corrupt in '%s'"), |
| unparsed_id); |
| |
| dirent->id = svn_fs_fs__id_parse(str, strlen(str), pool); |
| |
| svn_hash_sets(*entries_p, dirent->name, dirent); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return the cache object in FS responsible to storing the directory |
| * the NODEREV. If none exists, return NULL. */ |
| static svn_cache__t * |
| locate_dir_cache(svn_fs_t *fs, |
| node_revision_t *noderev) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| return svn_fs_fs__id_txn_id(noderev->id) |
| ? ffd->txn_dir_cache |
| : ffd->dir_cache; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__rep_contents_dir(apr_hash_t **entries_p, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_pool_t *pool) |
| { |
| const char *unparsed_id = NULL; |
| apr_hash_t *unparsed_entries, *parsed_entries; |
| |
| /* find the cache we may use */ |
| svn_cache__t *cache = locate_dir_cache(fs, noderev); |
| if (cache) |
| { |
| svn_boolean_t found; |
| |
| unparsed_id = svn_fs_fs__id_unparse(noderev->id, pool)->data; |
| SVN_ERR(svn_cache__get((void **) entries_p, &found, cache, |
| unparsed_id, pool)); |
| if (found) |
| return SVN_NO_ERROR; |
| } |
| |
| /* Read in the directory hash. */ |
| unparsed_entries = apr_hash_make(pool); |
| SVN_ERR(get_dir_contents(unparsed_entries, fs, noderev, pool)); |
| SVN_ERR(parse_dir_entries(&parsed_entries, unparsed_entries, |
| unparsed_id, pool)); |
| |
| /* Update the cache, if we are to use one. */ |
| if (cache) |
| SVN_ERR(svn_cache__set(cache, unparsed_id, parsed_entries, pool)); |
| |
| *entries_p = parsed_entries; |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__rep_contents_dir_entry(svn_fs_dirent_t **dirent, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| const char *name, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_boolean_t found = FALSE; |
| |
| /* find the cache we may use */ |
| svn_cache__t *cache = locate_dir_cache(fs, noderev); |
| if (cache) |
| { |
| const char *unparsed_id = |
| svn_fs_fs__id_unparse(noderev->id, scratch_pool)->data; |
| |
| /* Cache lookup. */ |
| SVN_ERR(svn_cache__get_partial((void **)dirent, |
| &found, |
| cache, |
| unparsed_id, |
| svn_fs_fs__extract_dir_entry, |
| (void*)name, |
| result_pool)); |
| } |
| |
| /* fetch data from disk if we did not find it in the cache */ |
| if (! found) |
| { |
| apr_hash_t *entries; |
| svn_fs_dirent_t *entry; |
| svn_fs_dirent_t *entry_copy = NULL; |
| |
| /* read the dir from the file system. It will probably be put it |
| into the cache for faster lookup in future calls. */ |
| SVN_ERR(svn_fs_fs__rep_contents_dir(&entries, fs, noderev, |
| scratch_pool)); |
| |
| /* find desired entry and return a copy in POOL, if found */ |
| entry = svn_hash_gets(entries, name); |
| if (entry != NULL) |
| { |
| entry_copy = apr_palloc(result_pool, sizeof(*entry_copy)); |
| entry_copy->name = apr_pstrdup(result_pool, entry->name); |
| entry_copy->id = svn_fs_fs__id_copy(entry->id, result_pool); |
| entry_copy->kind = entry->kind; |
| } |
| |
| *dirent = entry_copy; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__get_proplist(apr_hash_t **proplist_p, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *proplist; |
| svn_stream_t *stream; |
| |
| if (noderev->prop_rep && noderev->prop_rep->txn_id) |
| { |
| const char *filename = path_txn_node_props(fs, noderev->id, pool); |
| proplist = apr_hash_make(pool); |
| |
| SVN_ERR(svn_stream_open_readonly(&stream, filename, pool, pool)); |
| SVN_ERR(svn_hash_read2(proplist, stream, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_stream_close(stream)); |
| } |
| else if (noderev->prop_rep) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| representation_t *rep = noderev->prop_rep; |
| pair_cache_key_t key = { 0 }; |
| |
| key.revision = rep->revision; |
| key.second = rep->offset; |
| if (ffd->properties_cache && SVN_IS_VALID_REVNUM(rep->revision)) |
| { |
| svn_boolean_t is_cached; |
| SVN_ERR(svn_cache__get((void **) proplist_p, &is_cached, |
| ffd->properties_cache, &key, pool)); |
| if (is_cached) |
| return SVN_NO_ERROR; |
| } |
| |
| proplist = apr_hash_make(pool); |
| SVN_ERR(read_representation(&stream, fs, noderev->prop_rep, pool)); |
| SVN_ERR(svn_hash_read2(proplist, stream, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_stream_close(stream)); |
| |
| if (ffd->properties_cache && SVN_IS_VALID_REVNUM(rep->revision)) |
| SVN_ERR(svn_cache__set(ffd->properties_cache, &key, proplist, pool)); |
| } |
| else |
| { |
| /* return an empty prop list if the node doesn't have any props */ |
| proplist = apr_hash_make(pool); |
| } |
| |
| *proplist_p = proplist; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__file_length(svn_filesize_t *length, |
| node_revision_t *noderev, |
| apr_pool_t *pool) |
| { |
| representation_t *data_rep = noderev->data_rep; |
| if (!data_rep) |
| { |
| /* Treat "no representation" as "empty file". */ |
| *length = 0; |
| } |
| else if (data_rep->expanded_size) |
| { |
| /* Standard case: a non-empty file. */ |
| *length = data_rep->expanded_size; |
| } |
| else |
| { |
| /* Work around a FSFS format quirk (see issue #4554). |
| A plain representation may specify its EXPANDED LENGTH as "0" |
| in which case, the SIZE value is what we want. |
| |
| Because EXPANDED_LENGTH will also be 0 for empty files, while |
| SIZE is non-null, we need to check wether the content is |
| actually empty. We simply compare with the MD5 checksum of |
| empty content (sha-1 is not always available). |
| */ |
| if (svn_checksum_is_empty_checksum(data_rep->md5_checksum)) |
| { |
| /* Contents is empty. */ |
| *length = 0; |
| } |
| else |
| { |
| /* Contents is not empty, i.e. EXPANDED_LENGTH cannot be the |
| actual file length. */ |
| *length = data_rep->size; |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_boolean_t |
| svn_fs_fs__noderev_same_rep_key(representation_t *a, |
| representation_t *b) |
| { |
| if (a == b) |
| return TRUE; |
| |
| if (a == NULL || b == NULL) |
| return FALSE; |
| |
| if (a->offset != b->offset) |
| return FALSE; |
| |
| if (a->revision != b->revision) |
| return FALSE; |
| |
| if (a->uniquifier == b->uniquifier) |
| return TRUE; |
| |
| if (a->uniquifier == NULL || b->uniquifier == NULL) |
| return FALSE; |
| |
| return strcmp(a->uniquifier, b->uniquifier) == 0; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__file_checksum(svn_checksum_t **checksum, |
| node_revision_t *noderev, |
| svn_checksum_kind_t kind, |
| apr_pool_t *pool) |
| { |
| if (noderev->data_rep) |
| { |
| switch(kind) |
| { |
| case svn_checksum_md5: |
| *checksum = svn_checksum_dup(noderev->data_rep->md5_checksum, |
| pool); |
| break; |
| case svn_checksum_sha1: |
| *checksum = svn_checksum_dup(noderev->data_rep->sha1_checksum, |
| pool); |
| break; |
| default: |
| *checksum = NULL; |
| } |
| } |
| else |
| *checksum = NULL; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| representation_t * |
| svn_fs_fs__rep_copy(representation_t *rep, |
| apr_pool_t *pool) |
| { |
| representation_t *rep_new; |
| |
| if (rep == NULL) |
| return NULL; |
| |
| rep_new = apr_pcalloc(pool, sizeof(*rep_new)); |
| |
| memcpy(rep_new, rep, sizeof(*rep_new)); |
| rep_new->md5_checksum = svn_checksum_dup(rep->md5_checksum, pool); |
| rep_new->sha1_checksum = svn_checksum_dup(rep->sha1_checksum, pool); |
| rep_new->uniquifier = apr_pstrdup(pool, rep->uniquifier); |
| |
| return rep_new; |
| } |
| |
| /* Merge the internal-use-only CHANGE into a hash of public-FS |
| svn_fs_path_change2_t CHANGES, collapsing multiple changes into a |
| single summarical (is that real word?) change per path. Also keep |
| the COPYFROM_CACHE up to date with new adds and replaces. */ |
| static svn_error_t * |
| fold_change(apr_hash_t *changes, |
| const change_t *change, |
| apr_hash_t *copyfrom_cache) |
| { |
| apr_pool_t *pool = apr_hash_pool_get(changes); |
| svn_fs_path_change2_t *old_change, *new_change; |
| const char *path; |
| apr_size_t path_len = strlen(change->path); |
| |
| if ((old_change = apr_hash_get(changes, change->path, path_len))) |
| { |
| /* This path already exists in the hash, so we have to merge |
| this change into the already existing one. */ |
| |
| /* Sanity check: only allow NULL node revision ID in the |
| `reset' case. */ |
| if ((! change->noderev_id) && (change->kind != svn_fs_path_change_reset)) |
| return svn_error_create |
| (SVN_ERR_FS_CORRUPT, NULL, |
| _("Missing required node revision ID")); |
| |
| /* Sanity check: we should be talking about the same node |
| revision ID as our last change except where the last change |
| was a deletion. */ |
| if (change->noderev_id |
| && (! svn_fs_fs__id_eq(old_change->node_rev_id, change->noderev_id)) |
| && (old_change->change_kind != svn_fs_path_change_delete)) |
| return svn_error_create |
| (SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid change ordering: new node revision ID " |
| "without delete")); |
| |
| /* Sanity check: an add, replacement, or reset must be the first |
| thing to follow a deletion. */ |
| if ((old_change->change_kind == svn_fs_path_change_delete) |
| && (! ((change->kind == svn_fs_path_change_replace) |
| || (change->kind == svn_fs_path_change_reset) |
| || (change->kind == svn_fs_path_change_add)))) |
| return svn_error_create |
| (SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid change ordering: non-add change on deleted path")); |
| |
| /* Sanity check: an add can't follow anything except |
| a delete or reset. */ |
| if ((change->kind == svn_fs_path_change_add) |
| && (old_change->change_kind != svn_fs_path_change_delete) |
| && (old_change->change_kind != svn_fs_path_change_reset)) |
| return svn_error_create |
| (SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid change ordering: add change on preexisting path")); |
| |
| /* Now, merge that change in. */ |
| switch (change->kind) |
| { |
| case svn_fs_path_change_reset: |
| /* A reset here will simply remove the path change from the |
| hash. */ |
| old_change = NULL; |
| break; |
| |
| case svn_fs_path_change_delete: |
| if (old_change->change_kind == svn_fs_path_change_add) |
| { |
| /* If the path was introduced in this transaction via an |
| add, and we are deleting it, just remove the path |
| altogether. */ |
| old_change = NULL; |
| } |
| else |
| { |
| /* A deletion overrules all previous changes. */ |
| old_change->change_kind = svn_fs_path_change_delete; |
| old_change->text_mod = change->text_mod; |
| old_change->prop_mod = change->prop_mod; |
| old_change->copyfrom_rev = SVN_INVALID_REVNUM; |
| old_change->copyfrom_path = NULL; |
| } |
| break; |
| |
| case svn_fs_path_change_add: |
| case svn_fs_path_change_replace: |
| /* An add at this point must be following a previous delete, |
| so treat it just like a replace. */ |
| old_change->change_kind = svn_fs_path_change_replace; |
| old_change->node_rev_id = svn_fs_fs__id_copy(change->noderev_id, |
| pool); |
| old_change->text_mod = change->text_mod; |
| old_change->prop_mod = change->prop_mod; |
| if (change->copyfrom_rev == SVN_INVALID_REVNUM) |
| { |
| old_change->copyfrom_rev = SVN_INVALID_REVNUM; |
| old_change->copyfrom_path = NULL; |
| } |
| else |
| { |
| old_change->copyfrom_rev = change->copyfrom_rev; |
| old_change->copyfrom_path = apr_pstrdup(pool, |
| change->copyfrom_path); |
| } |
| break; |
| |
| case svn_fs_path_change_modify: |
| default: |
| if (change->text_mod) |
| old_change->text_mod = TRUE; |
| if (change->prop_mod) |
| old_change->prop_mod = TRUE; |
| break; |
| } |
| |
| /* Point our new_change to our (possibly modified) old_change. */ |
| new_change = old_change; |
| } |
| else |
| { |
| /* This change is new to the hash, so make a new public change |
| structure from the internal one (in the hash's pool), and dup |
| the path into the hash's pool, too. */ |
| new_change = apr_pcalloc(pool, sizeof(*new_change)); |
| new_change->node_rev_id = svn_fs_fs__id_copy(change->noderev_id, pool); |
| new_change->change_kind = change->kind; |
| new_change->text_mod = change->text_mod; |
| new_change->prop_mod = change->prop_mod; |
| /* In FSFS, copyfrom_known is *always* true, since we've always |
| * stored copyfroms in changed paths lists. */ |
| new_change->copyfrom_known = TRUE; |
| if (change->copyfrom_rev != SVN_INVALID_REVNUM) |
| { |
| new_change->copyfrom_rev = change->copyfrom_rev; |
| new_change->copyfrom_path = apr_pstrdup(pool, change->copyfrom_path); |
| } |
| else |
| { |
| new_change->copyfrom_rev = SVN_INVALID_REVNUM; |
| new_change->copyfrom_path = NULL; |
| } |
| } |
| |
| if (new_change) |
| new_change->node_kind = change->node_kind; |
| |
| /* Add (or update) this path. |
| |
| Note: this key might already be present, and it would be nice to |
| re-use its value, but there is no way to fetch it. The API makes no |
| guarantees that this (new) key will not be retained. Thus, we (again) |
| copy the key into the target pool to ensure a proper lifetime. */ |
| path = apr_pstrmemdup(pool, change->path, path_len); |
| apr_hash_set(changes, path, path_len, new_change); |
| |
| /* Update the copyfrom cache, if any. */ |
| if (copyfrom_cache) |
| { |
| apr_pool_t *copyfrom_pool = apr_hash_pool_get(copyfrom_cache); |
| const char *copyfrom_string = NULL, *copyfrom_key = path; |
| if (new_change) |
| { |
| if (SVN_IS_VALID_REVNUM(new_change->copyfrom_rev)) |
| copyfrom_string = apr_psprintf(copyfrom_pool, "%ld %s", |
| new_change->copyfrom_rev, |
| new_change->copyfrom_path); |
| else |
| copyfrom_string = ""; |
| } |
| /* We need to allocate a copy of the key in the copyfrom_pool if |
| * we're not doing a deletion and if it isn't already there. */ |
| if ( copyfrom_string |
| && ( ! apr_hash_count(copyfrom_cache) |
| || ! apr_hash_get(copyfrom_cache, copyfrom_key, path_len))) |
| copyfrom_key = apr_pstrmemdup(copyfrom_pool, copyfrom_key, path_len); |
| |
| apr_hash_set(copyfrom_cache, copyfrom_key, path_len, |
| copyfrom_string); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* The 256 is an arbitrary size large enough to hold the node id and the |
| * various flags. */ |
| #define MAX_CHANGE_LINE_LEN FSFS_MAX_PATH_LEN + 256 |
| |
| /* Read the next entry in the changes record from file FILE and store |
| the resulting change in *CHANGE_P. If there is no next record, |
| store NULL there. Perform all allocations from POOL. */ |
| static svn_error_t * |
| read_change(change_t **change_p, |
| apr_file_t *file, |
| apr_pool_t *pool) |
| { |
| char buf[MAX_CHANGE_LINE_LEN]; |
| apr_size_t len = sizeof(buf); |
| change_t *change; |
| char *str, *last_str = buf, *kind_str; |
| svn_error_t *err; |
| |
| /* Default return value. */ |
| *change_p = NULL; |
| |
| err = svn_io_read_length_line(file, buf, &len, pool); |
| |
| /* Check for a blank line. */ |
| if (err || (len == 0)) |
| { |
| if (err && APR_STATUS_IS_EOF(err->apr_err)) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| if ((len == 0) && (! err)) |
| return SVN_NO_ERROR; |
| return svn_error_trace(err); |
| } |
| |
| change = apr_pcalloc(pool, sizeof(*change)); |
| |
| /* Get the node-id of the change. */ |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid changes line in rev-file")); |
| |
| change->noderev_id = svn_fs_fs__id_parse(str, strlen(str), pool); |
| if (change->noderev_id == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid changes line in rev-file")); |
| |
| /* Get the change type. */ |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid changes line in rev-file")); |
| |
| /* Don't bother to check the format number before looking for |
| * node-kinds: just read them if you find them. */ |
| change->node_kind = svn_node_unknown; |
| kind_str = strchr(str, '-'); |
| if (kind_str) |
| { |
| /* Cap off the end of "str" (the action). */ |
| *kind_str = '\0'; |
| kind_str++; |
| if (strcmp(kind_str, KIND_FILE) == 0) |
| change->node_kind = svn_node_file; |
| else if (strcmp(kind_str, KIND_DIR) == 0) |
| change->node_kind = svn_node_dir; |
| else |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid changes line in rev-file")); |
| } |
| |
| if (strcmp(str, ACTION_MODIFY) == 0) |
| { |
| change->kind = svn_fs_path_change_modify; |
| } |
| else if (strcmp(str, ACTION_ADD) == 0) |
| { |
| change->kind = svn_fs_path_change_add; |
| } |
| else if (strcmp(str, ACTION_DELETE) == 0) |
| { |
| change->kind = svn_fs_path_change_delete; |
| } |
| else if (strcmp(str, ACTION_REPLACE) == 0) |
| { |
| change->kind = svn_fs_path_change_replace; |
| } |
| else if (strcmp(str, ACTION_RESET) == 0) |
| { |
| change->kind = svn_fs_path_change_reset; |
| } |
| else |
| { |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid change kind in rev file")); |
| } |
| |
| /* Get the text-mod flag. */ |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid changes line in rev-file")); |
| |
| if (strcmp(str, FLAG_TRUE) == 0) |
| { |
| change->text_mod = TRUE; |
| } |
| else if (strcmp(str, FLAG_FALSE) == 0) |
| { |
| change->text_mod = FALSE; |
| } |
| else |
| { |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid text-mod flag in rev-file")); |
| } |
| |
| /* Get the prop-mod flag. */ |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid changes line in rev-file")); |
| |
| if (strcmp(str, FLAG_TRUE) == 0) |
| { |
| change->prop_mod = TRUE; |
| } |
| else if (strcmp(str, FLAG_FALSE) == 0) |
| { |
| change->prop_mod = FALSE; |
| } |
| else |
| { |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid prop-mod flag in rev-file")); |
| } |
| |
| /* Get the changed path. */ |
| change->path = apr_pstrdup(pool, last_str); |
| |
| |
| /* Read the next line, the copyfrom line. */ |
| len = sizeof(buf); |
| SVN_ERR(svn_io_read_length_line(file, buf, &len, pool)); |
| |
| if (len == 0) |
| { |
| change->copyfrom_rev = SVN_INVALID_REVNUM; |
| change->copyfrom_path = NULL; |
| } |
| else |
| { |
| last_str = buf; |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (! str) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid changes line in rev-file")); |
| change->copyfrom_rev = SVN_STR_TO_REV(str); |
| |
| if (! last_str) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid changes line in rev-file")); |
| |
| change->copyfrom_path = apr_pstrdup(pool, last_str); |
| } |
| |
| *change_p = change; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Examine all the changed path entries in CHANGES and store them in |
| *CHANGED_PATHS. Folding is done to remove redundant or unnecessary |
| *data. Store a hash of paths to copyfrom "REV PATH" strings in |
| COPYFROM_HASH if it is non-NULL. If PREFOLDED is true, assume that |
| the changed-path entries have already been folded (by |
| write_final_changed_path_info) and may be out of order, so we shouldn't |
| remove children of replaced or deleted directories. Do all |
| allocations in POOL. */ |
| static svn_error_t * |
| process_changes(apr_hash_t *changed_paths, |
| apr_hash_t *copyfrom_cache, |
| apr_array_header_t *changes, |
| svn_boolean_t prefolded, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| int i; |
| |
| /* Read in the changes one by one, folding them into our local hash |
| as necessary. */ |
| |
| for (i = 0; i < changes->nelts; ++i) |
| { |
| change_t *change = APR_ARRAY_IDX(changes, i, change_t *); |
| |
| SVN_ERR(fold_change(changed_paths, change, copyfrom_cache)); |
| |
| /* Now, if our change was a deletion or replacement, we have to |
| blow away any changes thus far on paths that are (or, were) |
| children of this path. |
| ### i won't bother with another iteration pool here -- at |
| most we talking about a few extra dups of paths into what |
| is already a temporary subpool. |
| */ |
| |
| if (((change->kind == svn_fs_path_change_delete) |
| || (change->kind == svn_fs_path_change_replace)) |
| && ! prefolded) |
| { |
| apr_hash_index_t *hi; |
| |
| /* a potential child path must contain at least 2 more chars |
| (the path separator plus at least one char for the name). |
| Also, we should not assume that all paths have been normalized |
| i.e. some might have trailing path separators. |
| */ |
| apr_ssize_t change_path_len = strlen(change->path); |
| apr_ssize_t min_child_len = change_path_len == 0 |
| ? 1 |
| : change->path[change_path_len-1] == '/' |
| ? change_path_len + 1 |
| : change_path_len + 2; |
| |
| /* CAUTION: This is the inner loop of an O(n^2) algorithm. |
| The number of changes to process may be >> 1000. |
| Therefore, keep the inner loop as tight as possible. |
| */ |
| for (hi = apr_hash_first(iterpool, changed_paths); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| /* KEY is the path. */ |
| const void *path; |
| apr_ssize_t klen; |
| apr_hash_this(hi, &path, &klen, NULL); |
| |
| /* If we come across a child of our path, remove it. |
| Call svn_dirent_is_child only if there is a chance that |
| this is actually a sub-path. |
| */ |
| if ( klen >= min_child_len |
| && svn_dirent_is_child(change->path, path, iterpool)) |
| apr_hash_set(changed_paths, path, klen, NULL); |
| } |
| } |
| |
| /* Clear the per-iteration subpool. */ |
| svn_pool_clear(iterpool); |
| } |
| |
| /* Destroy the per-iteration subpool. */ |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Fetch all the changes from FILE and store them in *CHANGES. Do all |
| allocations in POOL. */ |
| static svn_error_t * |
| read_all_changes(apr_array_header_t **changes, |
| apr_file_t *file, |
| apr_pool_t *pool) |
| { |
| change_t *change; |
| |
| /* pre-allocate enough room for most change lists |
| (will be auto-expanded as necessary) */ |
| *changes = apr_array_make(pool, 30, sizeof(change_t *)); |
| |
| SVN_ERR(read_change(&change, file, pool)); |
| while (change) |
| { |
| APR_ARRAY_PUSH(*changes, change_t*) = change; |
| SVN_ERR(read_change(&change, file, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__txn_changes_fetch(apr_hash_t **changed_paths_p, |
| svn_fs_t *fs, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| apr_file_t *file; |
| apr_hash_t *changed_paths = apr_hash_make(pool); |
| apr_array_header_t *changes; |
| apr_pool_t *scratch_pool = svn_pool_create(pool); |
| |
| SVN_ERR(svn_io_file_open(&file, path_txn_changes(fs, txn_id, pool), |
| APR_READ | APR_BUFFERED, APR_OS_DEFAULT, pool)); |
| |
| SVN_ERR(read_all_changes(&changes, file, scratch_pool)); |
| SVN_ERR(process_changes(changed_paths, NULL, changes, FALSE, pool)); |
| svn_pool_destroy(scratch_pool); |
| |
| SVN_ERR(svn_io_file_close(file, pool)); |
| |
| *changed_paths_p = changed_paths; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Fetch the list of change in revision REV in FS and return it in *CHANGES. |
| * Allocate the result in POOL. |
| */ |
| static svn_error_t * |
| get_changes(apr_array_header_t **changes, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| apr_off_t changes_offset; |
| apr_file_t *revision_file; |
| svn_boolean_t found; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* try cache lookup first */ |
| |
| if (ffd->changes_cache) |
| { |
| SVN_ERR(svn_cache__get((void **) changes, &found, ffd->changes_cache, |
| &rev, pool)); |
| if (found) |
| return SVN_NO_ERROR; |
| } |
| |
| /* read changes from revision file */ |
| |
| SVN_ERR(ensure_revision_exists(fs, rev, pool)); |
| |
| SVN_ERR(open_pack_or_rev_file(&revision_file, fs, rev, pool)); |
| |
| SVN_ERR(get_root_changes_offset(NULL, &changes_offset, revision_file, fs, |
| rev, pool)); |
| |
| SVN_ERR(svn_io_file_seek(revision_file, APR_SET, &changes_offset, pool)); |
| SVN_ERR(read_all_changes(changes, revision_file, pool)); |
| |
| SVN_ERR(svn_io_file_close(revision_file, pool)); |
| |
| /* cache for future reference */ |
| |
| if (ffd->changes_cache) |
| SVN_ERR(svn_cache__set(ffd->changes_cache, &rev, *changes, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_fs_fs__paths_changed(apr_hash_t **changed_paths_p, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_hash_t *copyfrom_cache, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *changed_paths; |
| apr_array_header_t *changes; |
| apr_pool_t *scratch_pool = svn_pool_create(pool); |
| |
| SVN_ERR(get_changes(&changes, fs, rev, scratch_pool)); |
| |
| changed_paths = svn_hash__make(pool); |
| |
| SVN_ERR(process_changes(changed_paths, copyfrom_cache, changes, |
| TRUE, pool)); |
| svn_pool_destroy(scratch_pool); |
| |
| *changed_paths_p = changed_paths; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Copy a revision node-rev SRC into the current transaction TXN_ID in |
| the filesystem FS. This is only used to create the root of a transaction. |
| Allocations are from POOL. */ |
| static svn_error_t * |
| create_new_txn_noderev_from_rev(svn_fs_t *fs, |
| const char *txn_id, |
| svn_fs_id_t *src, |
| apr_pool_t *pool) |
| { |
| node_revision_t *noderev; |
| const char *node_id, *copy_id; |
| |
| SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs, src, pool)); |
| |
| if (svn_fs_fs__id_txn_id(noderev->id)) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Copying from transactions not allowed")); |
| |
| noderev->predecessor_id = noderev->id; |
| noderev->predecessor_count++; |
| noderev->copyfrom_path = NULL; |
| noderev->copyfrom_rev = SVN_INVALID_REVNUM; |
| |
| /* For the transaction root, the copyroot never changes. */ |
| |
| node_id = svn_fs_fs__id_node_id(noderev->id); |
| copy_id = svn_fs_fs__id_copy_id(noderev->id); |
| noderev->id = svn_fs_fs__id_txn_create(node_id, copy_id, txn_id, pool); |
| |
| return svn_fs_fs__put_node_revision(fs, noderev->id, noderev, TRUE, pool); |
| } |
| |
| /* A structure used by get_and_increment_txn_key_body(). */ |
| struct get_and_increment_txn_key_baton { |
| svn_fs_t *fs; |
| char *txn_id; |
| apr_pool_t *pool; |
| }; |
| |
| /* Callback used in the implementation of create_txn_dir(). This gets |
| the current base 36 value in PATH_TXN_CURRENT and increments it. |
| It returns the original value by the baton. */ |
| static svn_error_t * |
| get_and_increment_txn_key_body(void *baton, apr_pool_t *pool) |
| { |
| struct get_and_increment_txn_key_baton *cb = baton; |
| const char *txn_current_filename = path_txn_current(cb->fs, pool); |
| const char *tmp_filename; |
| char next_txn_id[MAX_KEY_SIZE+3]; |
| apr_size_t len; |
| |
| svn_stringbuf_t *buf; |
| SVN_ERR(read_content(&buf, txn_current_filename, cb->pool)); |
| |
| /* remove trailing newlines */ |
| svn_stringbuf_strip_whitespace(buf); |
| cb->txn_id = buf->data; |
| len = buf->len; |
| |
| /* Increment the key and add a trailing \n to the string so the |
| txn-current file has a newline in it. */ |
| svn_fs_fs__next_key(cb->txn_id, &len, next_txn_id); |
| next_txn_id[len] = '\n'; |
| ++len; |
| next_txn_id[len] = '\0'; |
| |
| SVN_ERR(svn_io_write_unique(&tmp_filename, |
| svn_dirent_dirname(txn_current_filename, pool), |
| next_txn_id, len, svn_io_file_del_none, pool)); |
| SVN_ERR(move_into_place(tmp_filename, txn_current_filename, |
| txn_current_filename, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Create a unique directory for a transaction in FS based on revision |
| REV. Return the ID for this transaction in *ID_P. Use a sequence |
| value in the transaction ID to prevent reuse of transaction IDs. */ |
| static svn_error_t * |
| create_txn_dir(const char **id_p, svn_fs_t *fs, svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| struct get_and_increment_txn_key_baton cb; |
| const char *txn_dir; |
| |
| /* Get the current transaction sequence value, which is a base-36 |
| number, from the txn-current file, and write an |
| incremented value back out to the file. Place the revision |
| number the transaction is based off into the transaction id. */ |
| cb.pool = pool; |
| cb.fs = fs; |
| SVN_ERR(with_txn_current_lock(fs, |
| get_and_increment_txn_key_body, |
| &cb, |
| pool)); |
| *id_p = apr_psprintf(pool, "%ld-%s", rev, cb.txn_id); |
| |
| txn_dir = svn_dirent_join_many(pool, |
| fs->path, |
| PATH_TXNS_DIR, |
| apr_pstrcat(pool, *id_p, PATH_EXT_TXN, |
| (char *)NULL), |
| NULL); |
| |
| return svn_io_dir_make(txn_dir, APR_OS_DEFAULT, pool); |
| } |
| |
| /* Create a unique directory for a transaction in FS based on revision |
| REV. Return the ID for this transaction in *ID_P. This |
| implementation is used in svn 1.4 and earlier repositories and is |
| kept in 1.5 and greater to support the --pre-1.4-compatible and |
| --pre-1.5-compatible repository creation options. Reused |
| transaction IDs are possible with this implementation. */ |
| static svn_error_t * |
| create_txn_dir_pre_1_5(const char **id_p, svn_fs_t *fs, svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| unsigned int i; |
| apr_pool_t *subpool; |
| const char *unique_path, *prefix; |
| |
| /* Try to create directories named "<txndir>/<rev>-<uniqueifier>.txn". */ |
| prefix = svn_dirent_join_many(pool, fs->path, PATH_TXNS_DIR, |
| apr_psprintf(pool, "%ld", rev), NULL); |
| |
| subpool = svn_pool_create(pool); |
| for (i = 1; i <= 99999; i++) |
| { |
| svn_error_t *err; |
| |
| svn_pool_clear(subpool); |
| unique_path = apr_psprintf(subpool, "%s-%u" PATH_EXT_TXN, prefix, i); |
| err = svn_io_dir_make(unique_path, APR_OS_DEFAULT, subpool); |
| if (! err) |
| { |
| /* We succeeded. Return the basename minus the ".txn" extension. */ |
| const char *name = svn_dirent_basename(unique_path, subpool); |
| *id_p = apr_pstrndup(pool, name, |
| strlen(name) - strlen(PATH_EXT_TXN)); |
| svn_pool_destroy(subpool); |
| return SVN_NO_ERROR; |
| } |
| if (! APR_STATUS_IS_EEXIST(err->apr_err)) |
| return svn_error_trace(err); |
| svn_error_clear(err); |
| } |
| |
| return svn_error_createf(SVN_ERR_IO_UNIQUE_NAMES_EXHAUSTED, |
| NULL, |
| _("Unable to create transaction directory " |
| "in '%s' for revision %ld"), |
| svn_dirent_local_style(fs->path, pool), |
| rev); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__create_txn(svn_fs_txn_t **txn_p, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_fs_txn_t *txn; |
| svn_fs_id_t *root_id; |
| |
| txn = apr_pcalloc(pool, sizeof(*txn)); |
| |
| /* Get the txn_id. */ |
| if (ffd->format >= SVN_FS_FS__MIN_TXN_CURRENT_FORMAT) |
| SVN_ERR(create_txn_dir(&txn->id, fs, rev, pool)); |
| else |
| SVN_ERR(create_txn_dir_pre_1_5(&txn->id, fs, rev, pool)); |
| |
| txn->fs = fs; |
| txn->base_rev = rev; |
| |
| txn->vtable = &txn_vtable; |
| *txn_p = txn; |
| |
| /* Create a new root node for this transaction. */ |
| SVN_ERR(svn_fs_fs__rev_get_root(&root_id, fs, rev, pool)); |
| SVN_ERR(create_new_txn_noderev_from_rev(fs, txn->id, root_id, pool)); |
| |
| /* Create an empty rev file. */ |
| SVN_ERR(svn_io_file_create(path_txn_proto_rev(fs, txn->id, pool), "", |
| pool)); |
| |
| /* Create an empty rev-lock file. */ |
| SVN_ERR(svn_io_file_create(path_txn_proto_rev_lock(fs, txn->id, pool), "", |
| pool)); |
| |
| /* Create an empty changes file. */ |
| SVN_ERR(svn_io_file_create(path_txn_changes(fs, txn->id, pool), "", |
| pool)); |
| |
| /* Create the next-ids file. */ |
| return svn_io_file_create(path_txn_next_ids(fs, txn->id, pool), "0 0\n", |
| pool); |
| } |
| |
| /* Store the property list for transaction TXN_ID in PROPLIST. |
| Perform temporary allocations in POOL. */ |
| static svn_error_t * |
| get_txn_proplist(apr_hash_t *proplist, |
| svn_fs_t *fs, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| svn_stream_t *stream; |
| |
| /* Check for issue #3696. (When we find and fix the cause, we can change |
| * this to an assertion.) */ |
| if (txn_id == NULL) |
| return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, |
| _("Internal error: a null transaction id was " |
| "passed to get_txn_proplist()")); |
| |
| /* Open the transaction properties file. */ |
| SVN_ERR(svn_stream_open_readonly(&stream, path_txn_props(fs, txn_id, pool), |
| pool, pool)); |
| |
| /* Read in the property list. */ |
| SVN_ERR(svn_hash_read2(proplist, stream, SVN_HASH_TERMINATOR, pool)); |
| |
| return svn_stream_close(stream); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__change_txn_prop(svn_fs_txn_t *txn, |
| const char *name, |
| const svn_string_t *value, |
| apr_pool_t *pool) |
| { |
| apr_array_header_t *props = apr_array_make(pool, 1, sizeof(svn_prop_t)); |
| svn_prop_t prop; |
| |
| prop.name = name; |
| prop.value = value; |
| APR_ARRAY_PUSH(props, svn_prop_t) = prop; |
| |
| return svn_fs_fs__change_txn_props(txn, props, pool); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__change_txn_props(svn_fs_txn_t *txn, |
| const apr_array_header_t *props, |
| apr_pool_t *pool) |
| { |
| const char *txn_prop_filename; |
| svn_stringbuf_t *buf; |
| svn_stream_t *stream; |
| apr_hash_t *txn_prop = apr_hash_make(pool); |
| int i; |
| svn_error_t *err; |
| |
| err = get_txn_proplist(txn_prop, txn->fs, txn->id, pool); |
| /* Here - and here only - we need to deal with the possibility that the |
| transaction property file doesn't yet exist. The rest of the |
| implementation assumes that the file exists, but we're called to set the |
| initial transaction properties as the transaction is being created. */ |
| if (err && (APR_STATUS_IS_ENOENT(err->apr_err))) |
| svn_error_clear(err); |
| else if (err) |
| return svn_error_trace(err); |
| |
| for (i = 0; i < props->nelts; i++) |
| { |
| svn_prop_t *prop = &APR_ARRAY_IDX(props, i, svn_prop_t); |
| |
| svn_hash_sets(txn_prop, prop->name, prop->value); |
| } |
| |
| /* Create a new version of the file and write out the new props. */ |
| /* Open the transaction properties file. */ |
| buf = svn_stringbuf_create_ensure(1024, pool); |
| stream = svn_stream_from_stringbuf(buf, pool); |
| SVN_ERR(svn_hash_write2(txn_prop, stream, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_stream_close(stream)); |
| SVN_ERR(svn_io_write_unique(&txn_prop_filename, |
| path_txn_dir(txn->fs, txn->id, pool), |
| buf->data, |
| buf->len, |
| svn_io_file_del_none, |
| pool)); |
| return svn_io_file_rename(txn_prop_filename, |
| path_txn_props(txn->fs, txn->id, pool), |
| pool); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__get_txn(transaction_t **txn_p, |
| svn_fs_t *fs, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| transaction_t *txn; |
| node_revision_t *noderev; |
| svn_fs_id_t *root_id; |
| |
| txn = apr_pcalloc(pool, sizeof(*txn)); |
| txn->proplist = apr_hash_make(pool); |
| |
| SVN_ERR(get_txn_proplist(txn->proplist, fs, txn_id, pool)); |
| root_id = svn_fs_fs__id_txn_create("0", "0", txn_id, pool); |
| |
| SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs, root_id, pool)); |
| |
| txn->root_id = svn_fs_fs__id_copy(noderev->id, pool); |
| txn->base_id = svn_fs_fs__id_copy(noderev->predecessor_id, pool); |
| txn->copies = NULL; |
| |
| *txn_p = txn; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write out the currently available next node_id NODE_ID and copy_id |
| COPY_ID for transaction TXN_ID in filesystem FS. The next node-id is |
| used both for creating new unique nodes for the given transaction, as |
| well as uniquifying representations. Perform temporary allocations in |
| POOL. */ |
| static svn_error_t * |
| write_next_ids(svn_fs_t *fs, |
| const char *txn_id, |
| const char *node_id, |
| const char *copy_id, |
| apr_pool_t *pool) |
| { |
| apr_file_t *file; |
| svn_stream_t *out_stream; |
| |
| SVN_ERR(svn_io_file_open(&file, path_txn_next_ids(fs, txn_id, pool), |
| APR_WRITE | APR_TRUNCATE, |
| APR_OS_DEFAULT, pool)); |
| |
| out_stream = svn_stream_from_aprfile2(file, TRUE, pool); |
| |
| SVN_ERR(svn_stream_printf(out_stream, pool, "%s %s\n", node_id, copy_id)); |
| |
| SVN_ERR(svn_stream_close(out_stream)); |
| return svn_io_file_close(file, pool); |
| } |
| |
| /* Find out what the next unique node-id and copy-id are for |
| transaction TXN_ID in filesystem FS. Store the results in *NODE_ID |
| and *COPY_ID. The next node-id is used both for creating new unique |
| nodes for the given transaction, as well as uniquifying representations. |
| Perform all allocations in POOL. */ |
| static svn_error_t * |
| read_next_ids(const char **node_id, |
| const char **copy_id, |
| svn_fs_t *fs, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| apr_file_t *file; |
| char buf[MAX_KEY_SIZE*2+3]; |
| apr_size_t limit; |
| char *str, *last_str = buf; |
| |
| SVN_ERR(svn_io_file_open(&file, path_txn_next_ids(fs, txn_id, pool), |
| APR_READ | APR_BUFFERED, APR_OS_DEFAULT, pool)); |
| |
| limit = sizeof(buf); |
| SVN_ERR(svn_io_read_length_line(file, buf, &limit, pool)); |
| |
| SVN_ERR(svn_io_file_close(file, pool)); |
| |
| /* Parse this into two separate strings. */ |
| |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (! str) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("next-id file corrupt")); |
| |
| *node_id = apr_pstrdup(pool, str); |
| |
| str = svn_cstring_tokenize(" ", &last_str); |
| if (! str) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("next-id file corrupt")); |
| |
| *copy_id = apr_pstrdup(pool, str); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Get a new and unique to this transaction node-id for transaction |
| TXN_ID in filesystem FS. Store the new node-id in *NODE_ID_P. |
| Node-ids are guaranteed to be unique to this transction, but may |
| not necessarily be sequential. Perform all allocations in POOL. */ |
| static svn_error_t * |
| get_new_txn_node_id(const char **node_id_p, |
| svn_fs_t *fs, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| const char *cur_node_id, *cur_copy_id; |
| char *node_id; |
| apr_size_t len; |
| |
| /* First read in the current next-ids file. */ |
| SVN_ERR(read_next_ids(&cur_node_id, &cur_copy_id, fs, txn_id, pool)); |
| |
| node_id = apr_pcalloc(pool, strlen(cur_node_id) + 2); |
| |
| len = strlen(cur_node_id); |
| svn_fs_fs__next_key(cur_node_id, &len, node_id); |
| |
| SVN_ERR(write_next_ids(fs, txn_id, node_id, cur_copy_id, pool)); |
| |
| *node_id_p = apr_pstrcat(pool, "_", cur_node_id, (char *)NULL); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__create_node(const svn_fs_id_t **id_p, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| const char *copy_id, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| const char *node_id; |
| const svn_fs_id_t *id; |
| |
| /* Get a new node-id for this node. */ |
| SVN_ERR(get_new_txn_node_id(&node_id, fs, txn_id, pool)); |
| |
| id = svn_fs_fs__id_txn_create(node_id, copy_id, txn_id, pool); |
| |
| noderev->id = id; |
| |
| SVN_ERR(svn_fs_fs__put_node_revision(fs, noderev->id, noderev, FALSE, pool)); |
| |
| *id_p = id; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__purge_txn(svn_fs_t *fs, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* Remove the shared transaction object associated with this transaction. */ |
| SVN_ERR(purge_shared_txn(fs, txn_id, pool)); |
| /* Remove the directory associated with this transaction. */ |
| SVN_ERR(svn_io_remove_dir2(path_txn_dir(fs, txn_id, pool), FALSE, |
| NULL, NULL, pool)); |
| if (ffd->format >= SVN_FS_FS__MIN_PROTOREVS_DIR_FORMAT) |
| { |
| /* Delete protorev and its lock, which aren't in the txn |
| directory. It's OK if they don't exist (for example, if this |
| is post-commit and the proto-rev has been moved into |
| place). */ |
| SVN_ERR(svn_io_remove_file2(path_txn_proto_rev(fs, txn_id, pool), |
| TRUE, pool)); |
| SVN_ERR(svn_io_remove_file2(path_txn_proto_rev_lock(fs, txn_id, pool), |
| TRUE, pool)); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_fs_fs__abort_txn(svn_fs_txn_t *txn, |
| apr_pool_t *pool) |
| { |
| SVN_ERR(svn_fs__check_fs(txn->fs, TRUE)); |
| |
| /* Now, purge the transaction. */ |
| SVN_ERR_W(svn_fs_fs__purge_txn(txn->fs, txn->id, pool), |
| apr_psprintf(pool, _("Transaction '%s' cleanup failed"), |
| txn->id)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_fs_fs__set_entry(svn_fs_t *fs, |
| const char *txn_id, |
| node_revision_t *parent_noderev, |
| const char *name, |
| const svn_fs_id_t *id, |
| svn_node_kind_t kind, |
| apr_pool_t *pool) |
| { |
| representation_t *rep = parent_noderev->data_rep; |
| const char *filename = path_txn_node_children(fs, parent_noderev->id, pool); |
| apr_file_t *file; |
| svn_stream_t *out; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| apr_pool_t *subpool = svn_pool_create(pool); |
| |
| if (!rep || !rep->txn_id) |
| { |
| const char *unique_suffix; |
| apr_hash_t *entries; |
| |
| /* Before we can modify the directory, we need to dump its old |
| contents into a mutable representation file. */ |
| SVN_ERR(svn_fs_fs__rep_contents_dir(&entries, fs, parent_noderev, |
| subpool)); |
| SVN_ERR(unparse_dir_entries(&entries, entries, subpool)); |
| SVN_ERR(svn_io_file_open(&file, filename, |
| APR_WRITE | APR_CREATE | APR_BUFFERED, |
| APR_OS_DEFAULT, pool)); |
| out = svn_stream_from_aprfile2(file, TRUE, pool); |
| SVN_ERR(svn_hash_write2(entries, out, SVN_HASH_TERMINATOR, subpool)); |
| |
| svn_pool_clear(subpool); |
| |
| /* Mark the node-rev's data rep as mutable. */ |
| rep = apr_pcalloc(pool, sizeof(*rep)); |
| rep->revision = SVN_INVALID_REVNUM; |
| rep->txn_id = txn_id; |
| |
| if (ffd->format >= SVN_FS_FS__MIN_REP_SHARING_FORMAT) |
| { |
| SVN_ERR(get_new_txn_node_id(&unique_suffix, fs, txn_id, pool)); |
| rep->uniquifier = apr_psprintf(pool, "%s/%s", txn_id, unique_suffix); |
| } |
| |
| parent_noderev->data_rep = rep; |
| SVN_ERR(svn_fs_fs__put_node_revision(fs, parent_noderev->id, |
| parent_noderev, FALSE, pool)); |
| } |
| else |
| { |
| /* The directory rep is already mutable, so just open it for append. */ |
| SVN_ERR(svn_io_file_open(&file, filename, APR_WRITE | APR_APPEND, |
| APR_OS_DEFAULT, pool)); |
| out = svn_stream_from_aprfile2(file, TRUE, pool); |
| } |
| |
| /* if we have a directory cache for this transaction, update it */ |
| if (ffd->txn_dir_cache) |
| { |
| /* build parameters: (name, new entry) pair */ |
| const char *key = |
| svn_fs_fs__id_unparse(parent_noderev->id, subpool)->data; |
| replace_baton_t baton; |
| |
| baton.name = name; |
| baton.new_entry = NULL; |
| |
| if (id) |
| { |
| baton.new_entry = apr_pcalloc(subpool, sizeof(*baton.new_entry)); |
| baton.new_entry->name = name; |
| baton.new_entry->kind = kind; |
| baton.new_entry->id = id; |
| } |
| |
| /* actually update the cached directory (if cached) */ |
| SVN_ERR(svn_cache__set_partial(ffd->txn_dir_cache, key, |
| svn_fs_fs__replace_dir_entry, &baton, |
| subpool)); |
| } |
| svn_pool_clear(subpool); |
| |
| /* Append an incremental hash entry for the entry change. */ |
| if (id) |
| { |
| const char *val = unparse_dir_entry(kind, id, subpool); |
| |
| SVN_ERR(svn_stream_printf(out, subpool, "K %" APR_SIZE_T_FMT "\n%s\n" |
| "V %" APR_SIZE_T_FMT "\n%s\n", |
| strlen(name), name, |
| strlen(val), val)); |
| } |
| else |
| { |
| SVN_ERR(svn_stream_printf(out, subpool, "D %" APR_SIZE_T_FMT "\n%s\n", |
| strlen(name), name)); |
| } |
| |
| SVN_ERR(svn_io_file_close(file, subpool)); |
| svn_pool_destroy(subpool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write a single change entry, path PATH, change CHANGE, and copyfrom |
| string COPYFROM, into the file specified by FILE. Only include the |
| node kind field if INCLUDE_NODE_KIND is true. All temporary |
| allocations are in POOL. */ |
| static svn_error_t * |
| write_change_entry(apr_file_t *file, |
| const char *path, |
| svn_fs_path_change2_t *change, |
| svn_boolean_t include_node_kind, |
| apr_pool_t *pool) |
| { |
| const char *idstr, *buf; |
| const char *change_string = NULL; |
| const char *kind_string = ""; |
| |
| switch (change->change_kind) |
| { |
| case svn_fs_path_change_modify: |
| change_string = ACTION_MODIFY; |
| break; |
| case svn_fs_path_change_add: |
| change_string = ACTION_ADD; |
| break; |
| case svn_fs_path_change_delete: |
| change_string = ACTION_DELETE; |
| break; |
| case svn_fs_path_change_replace: |
| change_string = ACTION_REPLACE; |
| break; |
| case svn_fs_path_change_reset: |
| change_string = ACTION_RESET; |
| break; |
| default: |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Invalid change type %d"), |
| change->change_kind); |
| } |
| |
| if (change->node_rev_id) |
| idstr = svn_fs_fs__id_unparse(change->node_rev_id, pool)->data; |
| else |
| idstr = ACTION_RESET; |
| |
| if (include_node_kind) |
| { |
| SVN_ERR_ASSERT(change->node_kind == svn_node_dir |
| || change->node_kind == svn_node_file); |
| kind_string = apr_psprintf(pool, "-%s", |
| change->node_kind == svn_node_dir |
| ? KIND_DIR : KIND_FILE); |
| } |
| buf = apr_psprintf(pool, "%s %s%s %s %s %s\n", |
| idstr, change_string, kind_string, |
| change->text_mod ? FLAG_TRUE : FLAG_FALSE, |
| change->prop_mod ? FLAG_TRUE : FLAG_FALSE, |
| path); |
| |
| SVN_ERR(svn_io_file_write_full(file, buf, strlen(buf), NULL, pool)); |
| |
| if (SVN_IS_VALID_REVNUM(change->copyfrom_rev)) |
| { |
| buf = apr_psprintf(pool, "%ld %s", change->copyfrom_rev, |
| change->copyfrom_path); |
| SVN_ERR(svn_io_file_write_full(file, buf, strlen(buf), NULL, pool)); |
| } |
| |
| return svn_io_file_write_full(file, "\n", 1, NULL, pool); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__add_change(svn_fs_t *fs, |
| const char *txn_id, |
| const char *path, |
| const svn_fs_id_t *id, |
| svn_fs_path_change_kind_t change_kind, |
| svn_boolean_t text_mod, |
| svn_boolean_t prop_mod, |
| svn_node_kind_t node_kind, |
| svn_revnum_t copyfrom_rev, |
| const char *copyfrom_path, |
| apr_pool_t *pool) |
| { |
| apr_file_t *file; |
| svn_fs_path_change2_t *change; |
| |
| SVN_ERR(svn_io_file_open(&file, path_txn_changes(fs, txn_id, pool), |
| APR_APPEND | APR_WRITE | APR_CREATE |
| | APR_BUFFERED, APR_OS_DEFAULT, pool)); |
| |
| change = svn_fs__path_change_create_internal(id, change_kind, pool); |
| change->text_mod = text_mod; |
| change->prop_mod = prop_mod; |
| change->node_kind = node_kind; |
| change->copyfrom_rev = copyfrom_rev; |
| change->copyfrom_path = apr_pstrdup(pool, copyfrom_path); |
| |
| SVN_ERR(write_change_entry(file, path, change, TRUE, pool)); |
| |
| return svn_io_file_close(file, pool); |
| } |
| |
| /* This baton is used by the representation writing streams. It keeps |
| track of the checksum information as well as the total size of the |
| representation so far. */ |
| struct rep_write_baton |
| { |
| /* The FS we are writing to. */ |
| svn_fs_t *fs; |
| |
| /* Actual file to which we are writing. */ |
| svn_stream_t *rep_stream; |
| |
| /* A stream from the delta combiner. Data written here gets |
| deltified, then eventually written to rep_stream. */ |
| svn_stream_t *delta_stream; |
| |
| /* Where is this representation header stored. */ |
| apr_off_t rep_offset; |
| |
| /* Start of the actual data. */ |
| apr_off_t delta_start; |
| |
| /* How many bytes have been written to this rep already. */ |
| svn_filesize_t rep_size; |
| |
| /* The node revision for which we're writing out info. */ |
| node_revision_t *noderev; |
| |
| /* Actual output file. */ |
| apr_file_t *file; |
| /* Lock 'cookie' used to unlock the output file once we've finished |
| writing to it. */ |
| void *lockcookie; |
| |
| svn_checksum_ctx_t *md5_checksum_ctx; |
| svn_checksum_ctx_t *sha1_checksum_ctx; |
| |
| apr_pool_t *pool; |
| |
| apr_pool_t *parent_pool; |
| }; |
| |
| /* Handler for the write method of the representation writable stream. |
| BATON is a rep_write_baton, DATA is the data to write, and *LEN is |
| the length of this data. */ |
| static svn_error_t * |
| rep_write_contents(void *baton, |
| const char *data, |
| apr_size_t *len) |
| { |
| struct rep_write_baton *b = baton; |
| |
| SVN_ERR(svn_checksum_update(b->md5_checksum_ctx, data, *len)); |
| SVN_ERR(svn_checksum_update(b->sha1_checksum_ctx, data, *len)); |
| b->rep_size += *len; |
| |
| /* If we are writing a delta, use that stream. */ |
| if (b->delta_stream) |
| return svn_stream_write(b->delta_stream, data, len); |
| else |
| return svn_stream_write(b->rep_stream, data, len); |
| } |
| |
| /* Given a node-revision NODEREV in filesystem FS, return the |
| representation in *REP to use as the base for a text representation |
| delta if PROPS is FALSE. If PROPS has been set, a suitable props |
| base representation will be returned. Perform temporary allocations |
| in *POOL. */ |
| static svn_error_t * |
| choose_delta_base(representation_t **rep, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| svn_boolean_t props, |
| apr_pool_t *pool) |
| { |
| int count; |
| int walk; |
| node_revision_t *base; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_boolean_t maybe_shared_rep = FALSE; |
| |
| /* If we have no predecessors, then use the empty stream as a |
| base. */ |
| if (! noderev->predecessor_count) |
| { |
| *rep = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Flip the rightmost '1' bit of the predecessor count to determine |
| which file rev (counting from 0) we want to use. (To see why |
| count & (count - 1) unsets the rightmost set bit, think about how |
| you decrement a binary number.) */ |
| count = noderev->predecessor_count; |
| count = count & (count - 1); |
| |
| /* We use skip delta for limiting the number of delta operations |
| along very long node histories. Close to HEAD however, we create |
| a linear history to minimize delta size. */ |
| walk = noderev->predecessor_count - count; |
| if (walk < (int)ffd->max_linear_deltification) |
| count = noderev->predecessor_count - 1; |
| |
| /* Finding the delta base over a very long distance can become extremely |
| expensive for very deep histories, possibly causing client timeouts etc. |
| OTOH, this is a rare operation and its gains are minimal. Lets simply |
| start deltification anew close every other 1000 changes or so. */ |
| if (walk > (int)ffd->max_deltification_walk) |
| { |
| *rep = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Walk back a number of predecessors equal to the difference |
| between count and the original predecessor count. (For example, |
| if noderev has ten predecessors and we want the eighth file rev, |
| walk back two predecessors.) */ |
| base = noderev; |
| while ((count++) < noderev->predecessor_count) |
| { |
| SVN_ERR(svn_fs_fs__get_node_revision(&base, fs, |
| base->predecessor_id, pool)); |
| |
| /* If there is a shared rep along the way, we need to limit the |
| * length of the deltification chain. |
| * |
| * Please note that copied nodes - such as branch directories - will |
| * look the same (false positive) while reps shared within the same |
| * revision will not be caught (false negative). |
| */ |
| if (props) |
| { |
| if ( base->prop_rep |
| && svn_fs_fs__id_rev(base->id) > base->prop_rep->revision) |
| maybe_shared_rep = TRUE; |
| } |
| else |
| { |
| if ( base->data_rep |
| && svn_fs_fs__id_rev(base->id) > base->data_rep->revision) |
| maybe_shared_rep = TRUE; |
| } |
| } |
| |
| /* return a suitable base representation */ |
| *rep = props ? base->prop_rep : base->data_rep; |
| |
| /* if we encountered a shared rep, it's parent chain may be different |
| * from the node-rev parent chain. */ |
| if (*rep && maybe_shared_rep) |
| { |
| /* Check whether the length of the deltification chain is acceptable. |
| * Otherwise, shared reps may form a non-skipping delta chain in |
| * extreme cases. */ |
| apr_pool_t *sub_pool = svn_pool_create(pool); |
| representation_t base_rep = **rep; |
| |
| /* Some reasonable limit, depending on how acceptable longer linear |
| * chains are in this repo. Also, allow for some minimal chain. */ |
| int max_chain_length = 2 * (int)ffd->max_linear_deltification + 2; |
| |
| /* re-use open files between iterations */ |
| svn_revnum_t rev_hint = SVN_INVALID_REVNUM; |
| apr_file_t *file_hint = NULL; |
| |
| /* follow the delta chain towards the end but for at most |
| * MAX_CHAIN_LENGTH steps. */ |
| for (; max_chain_length; --max_chain_length) |
| { |
| struct rep_state *rep_state; |
| struct rep_args *rep_args; |
| |
| SVN_ERR(create_rep_state_body(&rep_state, |
| &rep_args, |
| &file_hint, |
| &rev_hint, |
| &base_rep, |
| fs, |
| sub_pool)); |
| if (!rep_args->is_delta || !rep_args->base_revision) |
| break; |
| |
| base_rep.revision = rep_args->base_revision; |
| base_rep.offset = rep_args->base_offset; |
| base_rep.size = rep_args->base_length; |
| base_rep.txn_id = NULL; |
| } |
| |
| /* start new delta chain if the current one has grown too long */ |
| if (max_chain_length == 0) |
| *rep = NULL; |
| |
| svn_pool_destroy(sub_pool); |
| } |
| |
| /* verify that the reps don't form a degenerated '*/ |
| return SVN_NO_ERROR; |
| } |
| |
| /* Something went wrong and the pool for the rep write is being |
| cleared before we've finished writing the rep. So we need |
| to remove the rep from the protorevfile and we need to unlock |
| the protorevfile. */ |
| static apr_status_t |
| rep_write_cleanup(void *data) |
| { |
| struct rep_write_baton *b = data; |
| const char *txn_id = svn_fs_fs__id_txn_id(b->noderev->id); |
| svn_error_t *err; |
| |
| /* Truncate and close the protorevfile. */ |
| err = svn_io_file_trunc(b->file, b->rep_offset, b->pool); |
| err = svn_error_compose_create(err, svn_io_file_close(b->file, b->pool)); |
| |
| /* Remove our lock regardless of any preceeding errors so that the |
| being_written flag is always removed and stays consistent with the |
| file lock which will be removed no matter what since the pool is |
| going away. */ |
| err = svn_error_compose_create(err, unlock_proto_rev(b->fs, txn_id, |
| b->lockcookie, b->pool)); |
| if (err) |
| { |
| apr_status_t rc = err->apr_err; |
| svn_error_clear(err); |
| return rc; |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| |
| /* Get a rep_write_baton and store it in *WB_P for the representation |
| indicated by NODEREV in filesystem FS. Perform allocations in |
| POOL. Only appropriate for file contents, not for props or |
| directory contents. */ |
| static svn_error_t * |
| rep_write_get_baton(struct rep_write_baton **wb_p, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_pool_t *pool) |
| { |
| struct rep_write_baton *b; |
| apr_file_t *file; |
| representation_t *base_rep; |
| svn_stream_t *source; |
| const char *header; |
| svn_txdelta_window_handler_t wh; |
| void *whb; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| int diff_version = ffd->format >= SVN_FS_FS__MIN_SVNDIFF1_FORMAT ? 1 : 0; |
| |
| b = apr_pcalloc(pool, sizeof(*b)); |
| |
| b->sha1_checksum_ctx = svn_checksum_ctx_create(svn_checksum_sha1, pool); |
| b->md5_checksum_ctx = svn_checksum_ctx_create(svn_checksum_md5, pool); |
| |
| b->fs = fs; |
| b->parent_pool = pool; |
| b->pool = svn_pool_create(pool); |
| b->rep_size = 0; |
| b->noderev = noderev; |
| |
| /* Open the prototype rev file and seek to its end. */ |
| SVN_ERR(get_writable_proto_rev(&file, &b->lockcookie, |
| fs, svn_fs_fs__id_txn_id(noderev->id), |
| b->pool)); |
| |
| b->file = file; |
| b->rep_stream = svn_stream_from_aprfile2(file, TRUE, b->pool); |
| |
| SVN_ERR(get_file_offset(&b->rep_offset, file, b->pool)); |
| |
| /* Get the base for this delta. */ |
| SVN_ERR(choose_delta_base(&base_rep, fs, noderev, FALSE, b->pool)); |
| SVN_ERR(read_representation(&source, fs, base_rep, b->pool)); |
| |
| /* Write out the rep header. */ |
| if (base_rep) |
| { |
| header = apr_psprintf(b->pool, REP_DELTA " %ld %" APR_OFF_T_FMT " %" |
| SVN_FILESIZE_T_FMT "\n", |
| base_rep->revision, base_rep->offset, |
| base_rep->size); |
| } |
| else |
| { |
| header = REP_DELTA "\n"; |
| } |
| SVN_ERR(svn_io_file_write_full(file, header, strlen(header), NULL, |
| b->pool)); |
| |
| /* Now determine the offset of the actual svndiff data. */ |
| SVN_ERR(get_file_offset(&b->delta_start, file, b->pool)); |
| |
| /* Cleanup in case something goes wrong. */ |
| apr_pool_cleanup_register(b->pool, b, rep_write_cleanup, |
| apr_pool_cleanup_null); |
| |
| /* Prepare to write the svndiff data. */ |
| svn_txdelta_to_svndiff3(&wh, |
| &whb, |
| b->rep_stream, |
| diff_version, |
| SVN_DELTA_COMPRESSION_LEVEL_DEFAULT, |
| pool); |
| |
| b->delta_stream = svn_txdelta_target_push(wh, whb, source, b->pool); |
| |
| *wb_p = b; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* For the hash REP->SHA1, try to find an already existing representation |
| in FS and return it in *OUT_REP. If no such representation exists or |
| if rep sharing has been disabled for FS, NULL will be returned. Since |
| there may be new duplicate representations within the same uncommitted |
| revision, those can be passed in REPS_HASH (maps a sha1 digest onto |
| representation_t*), otherwise pass in NULL for REPS_HASH. |
| POOL will be used for allocations. The lifetime of the returned rep is |
| limited by both, POOL and REP lifetime. |
| */ |
| static svn_error_t * |
| get_shared_rep(representation_t **old_rep, |
| svn_fs_t *fs, |
| representation_t *rep, |
| apr_hash_t *reps_hash, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* Return NULL, if rep sharing has been disabled. */ |
| *old_rep = NULL; |
| if (!ffd->rep_sharing_allowed) |
| return SVN_NO_ERROR; |
| |
| /* Check and see if we already have a representation somewhere that's |
| identical to the one we just wrote out. Start with the hash lookup |
| because it is cheepest. */ |
| if (reps_hash) |
| *old_rep = apr_hash_get(reps_hash, |
| rep->sha1_checksum->digest, |
| APR_SHA1_DIGESTSIZE); |
| |
| /* If we haven't found anything yet, try harder and consult our DB. */ |
| if (*old_rep == NULL) |
| { |
| err = svn_fs_fs__get_rep_reference(old_rep, fs, rep->sha1_checksum, |
| pool); |
| /* ### Other error codes that we shouldn't mask out? */ |
| if (err == SVN_NO_ERROR) |
| { |
| if (*old_rep) |
| SVN_ERR(verify_walker(*old_rep, NULL, fs, pool)); |
| } |
| else if (err->apr_err == SVN_ERR_FS_CORRUPT |
| || SVN_ERROR_IN_CATEGORY(err->apr_err, |
| SVN_ERR_MALFUNC_CATEGORY_START)) |
| { |
| /* Fatal error; don't mask it. |
| |
| In particular, this block is triggered when the rep-cache refers |
| to revisions in the future. We signal that as a corruption situation |
| since, once those revisions are less than youngest (because of more |
| commits), the rep-cache would be invalid. |
| */ |
| SVN_ERR(err); |
| } |
| else |
| { |
| /* Something's wrong with the rep-sharing index. We can continue |
| without rep-sharing, but warn. |
| */ |
| (fs->warning)(fs->warning_baton, err); |
| svn_error_clear(err); |
| *old_rep = NULL; |
| } |
| } |
| |
| /* look for intra-revision matches (usually data reps but not limited |
| to them in case props happen to look like some data rep) |
| */ |
| if (*old_rep == NULL && rep->txn_id) |
| { |
| svn_node_kind_t kind; |
| const char *file_name |
| = path_txn_sha1(fs, rep->txn_id, rep->sha1_checksum, pool); |
| |
| /* in our txn, is there a rep file named with the wanted SHA1? |
| If so, read it and use that rep. |
| */ |
| SVN_ERR(svn_io_check_path(file_name, &kind, pool)); |
| if (kind == svn_node_file) |
| { |
| svn_stringbuf_t *rep_string; |
| SVN_ERR(svn_stringbuf_from_file2(&rep_string, file_name, pool)); |
| SVN_ERR(read_rep_offsets_body(old_rep, rep_string->data, |
| rep->txn_id, FALSE, pool)); |
| } |
| } |
| |
| if (!*old_rep) |
| return SVN_NO_ERROR; |
| |
| /* We don't want 0-length PLAIN representations to replace non-0-length |
| ones (see issue #4554). Also, this doubles as a simple guard against |
| general rep-cache induced corruption. */ |
| if ( ((*old_rep)->expanded_size != rep->expanded_size) |
| || ((*old_rep)->size != rep->size)) |
| { |
| *old_rep = NULL; |
| } |
| else |
| { |
| /* Add information that is missing in the cached data. |
| Use the old rep for this content. */ |
| (*old_rep)->md5_checksum = rep->md5_checksum; |
| (*old_rep)->uniquifier = rep->uniquifier; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Close handler for the representation write stream. BATON is a |
| rep_write_baton. Writes out a new node-rev that correctly |
| references the representation we just finished writing. */ |
| static svn_error_t * |
| rep_write_contents_close(void *baton) |
| { |
| struct rep_write_baton *b = baton; |
| const char *unique_suffix; |
| representation_t *rep; |
| representation_t *old_rep; |
| apr_off_t offset; |
| fs_fs_data_t *ffd = b->fs->fsap_data; |
| |
| rep = apr_pcalloc(b->parent_pool, sizeof(*rep)); |
| rep->offset = b->rep_offset; |
| |
| /* Close our delta stream so the last bits of svndiff are written |
| out. */ |
| if (b->delta_stream) |
| SVN_ERR(svn_stream_close(b->delta_stream)); |
| |
| /* Determine the length of the svndiff data. */ |
| SVN_ERR(get_file_offset(&offset, b->file, b->pool)); |
| rep->size = offset - b->delta_start; |
| |
| /* Fill in the rest of the representation field. */ |
| rep->expanded_size = b->rep_size; |
| rep->txn_id = svn_fs_fs__id_txn_id(b->noderev->id); |
| |
| if (ffd->format >= SVN_FS_FS__MIN_REP_SHARING_FORMAT) |
| { |
| SVN_ERR(get_new_txn_node_id(&unique_suffix, b->fs, rep->txn_id, b->pool)); |
| rep->uniquifier = apr_psprintf(b->parent_pool, "%s/%s", rep->txn_id, |
| unique_suffix); |
| } |
| rep->revision = SVN_INVALID_REVNUM; |
| |
| /* Finalize the checksum. */ |
| SVN_ERR(svn_checksum_final(&rep->md5_checksum, b->md5_checksum_ctx, |
| b->parent_pool)); |
| SVN_ERR(svn_checksum_final(&rep->sha1_checksum, b->sha1_checksum_ctx, |
| b->parent_pool)); |
| |
| /* Check and see if we already have a representation somewhere that's |
| identical to the one we just wrote out. */ |
| SVN_ERR(get_shared_rep(&old_rep, b->fs, rep, NULL, b->parent_pool)); |
| |
| if (old_rep) |
| { |
| /* We need to erase from the protorev the data we just wrote. */ |
| SVN_ERR(svn_io_file_trunc(b->file, b->rep_offset, b->pool)); |
| |
| /* Use the old rep for this content. */ |
| b->noderev->data_rep = old_rep; |
| } |
| else |
| { |
| /* Write out our cosmetic end marker. */ |
| SVN_ERR(svn_stream_puts(b->rep_stream, "ENDREP\n")); |
| |
| b->noderev->data_rep = rep; |
| } |
| |
| /* Remove cleanup callback. */ |
| apr_pool_cleanup_kill(b->pool, b, rep_write_cleanup); |
| |
| /* Write out the new node-rev information. */ |
| SVN_ERR(svn_fs_fs__put_node_revision(b->fs, b->noderev->id, b->noderev, FALSE, |
| b->pool)); |
| if (!old_rep) |
| SVN_ERR(store_sha1_rep_mapping(b->fs, b->noderev, b->pool)); |
| |
| SVN_ERR(svn_io_file_close(b->file, b->pool)); |
| SVN_ERR(unlock_proto_rev(b->fs, rep->txn_id, b->lockcookie, b->pool)); |
| svn_pool_destroy(b->pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Store a writable stream in *CONTENTS_P that will receive all data |
| written and store it as the file data representation referenced by |
| NODEREV in filesystem FS. Perform temporary allocations in |
| POOL. Only appropriate for file data, not props or directory |
| contents. */ |
| static svn_error_t * |
| set_representation(svn_stream_t **contents_p, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_pool_t *pool) |
| { |
| struct rep_write_baton *wb; |
| |
| if (! svn_fs_fs__id_txn_id(noderev->id)) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Attempted to write to non-transaction '%s'"), |
| svn_fs_fs__id_unparse(noderev->id, pool)->data); |
| |
| SVN_ERR(rep_write_get_baton(&wb, fs, noderev, pool)); |
| |
| *contents_p = svn_stream_create(wb, pool); |
| svn_stream_set_write(*contents_p, rep_write_contents); |
| svn_stream_set_close(*contents_p, rep_write_contents_close); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__set_contents(svn_stream_t **stream, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_pool_t *pool) |
| { |
| if (noderev->kind != svn_node_file) |
| return svn_error_create(SVN_ERR_FS_NOT_FILE, NULL, |
| _("Can't set text contents of a directory")); |
| |
| return set_representation(stream, fs, noderev, pool); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__create_successor(const svn_fs_id_t **new_id_p, |
| svn_fs_t *fs, |
| const svn_fs_id_t *old_idp, |
| node_revision_t *new_noderev, |
| const char *copy_id, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| const svn_fs_id_t *id; |
| |
| if (! copy_id) |
| copy_id = svn_fs_fs__id_copy_id(old_idp); |
| id = svn_fs_fs__id_txn_create(svn_fs_fs__id_node_id(old_idp), copy_id, |
| txn_id, pool); |
| |
| new_noderev->id = id; |
| |
| if (! new_noderev->copyroot_path) |
| { |
| new_noderev->copyroot_path = apr_pstrdup(pool, |
| new_noderev->created_path); |
| new_noderev->copyroot_rev = svn_fs_fs__id_rev(new_noderev->id); |
| } |
| |
| SVN_ERR(svn_fs_fs__put_node_revision(fs, new_noderev->id, new_noderev, FALSE, |
| pool)); |
| |
| *new_id_p = id; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__set_proplist(svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_hash_t *proplist, |
| apr_pool_t *pool) |
| { |
| const char *filename = path_txn_node_props(fs, noderev->id, pool); |
| apr_file_t *file; |
| svn_stream_t *out; |
| |
| /* Dump the property list to the mutable property file. */ |
| SVN_ERR(svn_io_file_open(&file, filename, |
| APR_WRITE | APR_CREATE | APR_TRUNCATE |
| | APR_BUFFERED, APR_OS_DEFAULT, pool)); |
| out = svn_stream_from_aprfile2(file, TRUE, pool); |
| SVN_ERR(svn_hash_write2(proplist, out, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_io_file_close(file, pool)); |
| |
| /* Mark the node-rev's prop rep as mutable, if not already done. */ |
| if (!noderev->prop_rep || !noderev->prop_rep->txn_id) |
| { |
| noderev->prop_rep = apr_pcalloc(pool, sizeof(*noderev->prop_rep)); |
| noderev->prop_rep->txn_id = svn_fs_fs__id_txn_id(noderev->id); |
| SVN_ERR(svn_fs_fs__put_node_revision(fs, noderev->id, noderev, FALSE, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Read the 'current' file for filesystem FS and store the next |
| available node id in *NODE_ID, and the next available copy id in |
| *COPY_ID. Allocations are performed from POOL. */ |
| static svn_error_t * |
| get_next_revision_ids(const char **node_id, |
| const char **copy_id, |
| svn_fs_t *fs, |
| apr_pool_t *pool) |
| { |
| char *buf; |
| char *str; |
| svn_stringbuf_t *content; |
| |
| SVN_ERR(read_content(&content, svn_fs_fs__path_current(fs, pool), pool)); |
| buf = content->data; |
| |
| str = svn_cstring_tokenize(" ", &buf); |
| if (! str) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Corrupt 'current' file")); |
| |
| str = svn_cstring_tokenize(" ", &buf); |
| if (! str) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Corrupt 'current' file")); |
| |
| *node_id = apr_pstrdup(pool, str); |
| |
| str = svn_cstring_tokenize(" \n", &buf); |
| if (! str) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Corrupt 'current' file")); |
| |
| *copy_id = apr_pstrdup(pool, str); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* This baton is used by the stream created for write_hash_rep. */ |
| struct write_hash_baton |
| { |
| svn_stream_t *stream; |
| |
| apr_size_t size; |
| |
| svn_checksum_ctx_t *md5_ctx; |
| svn_checksum_ctx_t *sha1_ctx; |
| }; |
| |
| /* The handler for the write_hash_rep stream. BATON is a |
| write_hash_baton, DATA has the data to write and *LEN is the number |
| of bytes to write. */ |
| static svn_error_t * |
| write_hash_handler(void *baton, |
| const char *data, |
| apr_size_t *len) |
| { |
| struct write_hash_baton *whb = baton; |
| |
| SVN_ERR(svn_checksum_update(whb->md5_ctx, data, *len)); |
| SVN_ERR(svn_checksum_update(whb->sha1_ctx, data, *len)); |
| |
| SVN_ERR(svn_stream_write(whb->stream, data, len)); |
| whb->size += *len; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write out the hash HASH as a text representation to file FILE. In |
| the process, record position, the total size of the dump and MD5 as |
| well as SHA1 in REP. If rep sharing has been enabled and REPS_HASH |
| is not NULL, it will be used in addition to the on-disk cache to find |
| earlier reps with the same content. When such existing reps can be |
| found, we will truncate the one just written from the file and return |
| the existing rep. Perform temporary allocations in POOL. */ |
| static svn_error_t * |
| write_hash_rep(representation_t *rep, |
| apr_file_t *file, |
| apr_hash_t *hash, |
| svn_fs_t *fs, |
| apr_hash_t *reps_hash, |
| apr_pool_t *pool) |
| { |
| svn_stream_t *stream; |
| struct write_hash_baton *whb; |
| representation_t *old_rep; |
| |
| SVN_ERR(get_file_offset(&rep->offset, file, pool)); |
| |
| whb = apr_pcalloc(pool, sizeof(*whb)); |
| |
| whb->stream = svn_stream_from_aprfile2(file, TRUE, pool); |
| whb->size = 0; |
| whb->md5_ctx = svn_checksum_ctx_create(svn_checksum_md5, pool); |
| whb->sha1_ctx = svn_checksum_ctx_create(svn_checksum_sha1, pool); |
| |
| stream = svn_stream_create(whb, pool); |
| svn_stream_set_write(stream, write_hash_handler); |
| |
| SVN_ERR(svn_stream_puts(whb->stream, "PLAIN\n")); |
| |
| SVN_ERR(svn_hash_write2(hash, stream, SVN_HASH_TERMINATOR, pool)); |
| |
| /* Store the results. */ |
| SVN_ERR(svn_checksum_final(&rep->md5_checksum, whb->md5_ctx, pool)); |
| SVN_ERR(svn_checksum_final(&rep->sha1_checksum, whb->sha1_ctx, pool)); |
| |
| /* Check and see if we already have a representation somewhere that's |
| identical to the one we just wrote out. */ |
| SVN_ERR(get_shared_rep(&old_rep, fs, rep, reps_hash, pool)); |
| |
| if (old_rep) |
| { |
| /* We need to erase from the protorev the data we just wrote. */ |
| SVN_ERR(svn_io_file_trunc(file, rep->offset, pool)); |
| |
| /* Use the old rep for this content. */ |
| memcpy(rep, old_rep, sizeof (*rep)); |
| } |
| else |
| { |
| /* Write out our cosmetic end marker. */ |
| SVN_ERR(svn_stream_puts(whb->stream, "ENDREP\n")); |
| |
| /* update the representation */ |
| rep->size = whb->size; |
| rep->expanded_size = whb->size; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write out the hash HASH pertaining to the NODEREV in FS as a deltified |
| text representation to file FILE. In the process, record the total size |
| and the md5 digest in REP. If rep sharing has been enabled and REPS_HASH |
| is not NULL, it will be used in addition to the on-disk cache to find |
| earlier reps with the same content. When such existing reps can be found, |
| we will truncate the one just written from the file and return the existing |
| rep. If PROPS is set, assume that we want to a props representation as |
| the base for our delta. Perform temporary allocations in POOL. */ |
| static svn_error_t * |
| write_hash_delta_rep(representation_t *rep, |
| apr_file_t *file, |
| apr_hash_t *hash, |
| svn_fs_t *fs, |
| node_revision_t *noderev, |
| apr_hash_t *reps_hash, |
| svn_boolean_t props, |
| apr_pool_t *pool) |
| { |
| svn_txdelta_window_handler_t diff_wh; |
| void *diff_whb; |
| |
| svn_stream_t *file_stream; |
| svn_stream_t *stream; |
| representation_t *base_rep; |
| representation_t *old_rep; |
| svn_stream_t *source; |
| const char *header; |
| |
| apr_off_t rep_end = 0; |
| apr_off_t delta_start = 0; |
| |
| struct write_hash_baton *whb; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| int diff_version = ffd->format >= SVN_FS_FS__MIN_SVNDIFF1_FORMAT ? 1 : 0; |
| |
| /* Get the base for this delta. */ |
| SVN_ERR(choose_delta_base(&base_rep, fs, noderev, props, pool)); |
| SVN_ERR(read_representation(&source, fs, base_rep, pool)); |
| |
| SVN_ERR(get_file_offset(&rep->offset, file, pool)); |
| |
| /* Write out the rep header. */ |
| if (base_rep) |
| { |
| header = apr_psprintf(pool, REP_DELTA " %ld %" APR_OFF_T_FMT " %" |
| SVN_FILESIZE_T_FMT "\n", |
| base_rep->revision, base_rep->offset, |
| base_rep->size); |
| } |
| else |
| { |
| header = REP_DELTA "\n"; |
| } |
| SVN_ERR(svn_io_file_write_full(file, header, strlen(header), NULL, |
| pool)); |
| |
| SVN_ERR(get_file_offset(&delta_start, file, pool)); |
| file_stream = svn_stream_from_aprfile2(file, TRUE, pool); |
| |
| /* Prepare to write the svndiff data. */ |
| svn_txdelta_to_svndiff3(&diff_wh, |
| &diff_whb, |
| file_stream, |
| diff_version, |
| SVN_DELTA_COMPRESSION_LEVEL_DEFAULT, |
| pool); |
| |
| whb = apr_pcalloc(pool, sizeof(*whb)); |
| whb->stream = svn_txdelta_target_push(diff_wh, diff_whb, source, pool); |
| whb->size = 0; |
| whb->md5_ctx = svn_checksum_ctx_create(svn_checksum_md5, pool); |
| whb->sha1_ctx = svn_checksum_ctx_create(svn_checksum_sha1, pool); |
| |
| /* serialize the hash */ |
| stream = svn_stream_create(whb, pool); |
| svn_stream_set_write(stream, write_hash_handler); |
| |
| SVN_ERR(svn_hash_write2(hash, stream, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_stream_close(whb->stream)); |
| |
| /* Store the results. */ |
| SVN_ERR(svn_checksum_final(&rep->md5_checksum, whb->md5_ctx, pool)); |
| SVN_ERR(svn_checksum_final(&rep->sha1_checksum, whb->sha1_ctx, pool)); |
| |
| /* Check and see if we already have a representation somewhere that's |
| identical to the one we just wrote out. */ |
| SVN_ERR(get_shared_rep(&old_rep, fs, rep, reps_hash, pool)); |
| |
| if (old_rep) |
| { |
| /* We need to erase from the protorev the data we just wrote. */ |
| SVN_ERR(svn_io_file_trunc(file, rep->offset, pool)); |
| |
| /* Use the old rep for this content. */ |
| memcpy(rep, old_rep, sizeof (*rep)); |
| } |
| else |
| { |
| /* Write out our cosmetic end marker. */ |
| SVN_ERR(get_file_offset(&rep_end, file, pool)); |
| SVN_ERR(svn_stream_puts(file_stream, "ENDREP\n")); |
| |
| /* update the representation */ |
| rep->expanded_size = whb->size; |
| rep->size = rep_end - delta_start; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Sanity check ROOT_NODEREV, a candidate for being the root node-revision |
| of (not yet committed) revision REV in FS. Use POOL for temporary |
| allocations. |
| |
| If you change this function, consider updating svn_fs_fs__verify() too. |
| */ |
| static svn_error_t * |
| validate_root_noderev(svn_fs_t *fs, |
| node_revision_t *root_noderev, |
| svn_revnum_t rev, |
| apr_pool_t *pool) |
| { |
| svn_revnum_t head_revnum = rev-1; |
| int head_predecessor_count; |
| |
| SVN_ERR_ASSERT(rev > 0); |
| |
| /* Compute HEAD_PREDECESSOR_COUNT. */ |
| { |
| svn_fs_root_t *head_revision; |
| const svn_fs_id_t *head_root_id; |
| node_revision_t *head_root_noderev; |
| |
| /* Get /@HEAD's noderev. */ |
| SVN_ERR(svn_fs_fs__revision_root(&head_revision, fs, head_revnum, pool)); |
| SVN_ERR(svn_fs_fs__node_id(&head_root_id, head_revision, "/", pool)); |
| SVN_ERR(svn_fs_fs__get_node_revision(&head_root_noderev, fs, head_root_id, |
| pool)); |
| |
| head_predecessor_count = head_root_noderev->predecessor_count; |
| } |
| |
| /* Check that the root noderev's predecessor count equals REV. |
| |
| This kind of corruption was seen on svn.apache.org (both on |
| the root noderev and on other fspaths' noderevs); see |
| issue #4129. |
| |
| Normally (rev == root_noderev->predecessor_count), but here we |
| use a more roundabout check that should only trigger on new instances |
| of the corruption, rather then trigger on each and every new commit |
| to a repository that has triggered the bug somewhere in its root |
| noderev's history. |
| */ |
| if (root_noderev->predecessor_count != -1 |
| && (root_noderev->predecessor_count - head_predecessor_count) |
| != (rev - head_revnum)) |
| { |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("predecessor count for " |
| "the root node-revision is wrong: " |
| "found (%d+%ld != %d), committing r%ld"), |
| head_predecessor_count, |
| rev - head_revnum, /* This is equal to 1. */ |
| root_noderev->predecessor_count, |
| rev); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Copy a node-revision specified by id ID in fileystem FS from a |
| transaction into the proto-rev-file FILE. Set *NEW_ID_P to a |
| pointer to the new node-id which will be allocated in POOL. |
| If this is a directory, copy all children as well. |
| |
| START_NODE_ID and START_COPY_ID are |
| the first available node and copy ids for this filesystem, for older |
| FS formats. |
| |
| REV is the revision number that this proto-rev-file will represent. |
| |
| INITIAL_OFFSET is the offset of the proto-rev-file on entry to |
| commit_body. |
| |
| If REPS_TO_CACHE is not NULL, append to it a copy (allocated in |
| REPS_POOL) of each data rep that is new in this revision. |
| |
| If REPS_HASH is not NULL, append copies (allocated in REPS_POOL) |
| of the representations of each property rep that is new in this |
| revision. |
| |
| AT_ROOT is true if the node revision being written is the root |
| node-revision. It is only controls additional sanity checking |
| logic. |
| |
| Temporary allocations are also from POOL. */ |
| static svn_error_t * |
| write_final_rev(const svn_fs_id_t **new_id_p, |
| apr_file_t *file, |
| svn_revnum_t rev, |
| svn_fs_t *fs, |
| const svn_fs_id_t *id, |
| const char *start_node_id, |
| const char *start_copy_id, |
| apr_off_t initial_offset, |
| apr_array_header_t *reps_to_cache, |
| apr_hash_t *reps_hash, |
| apr_pool_t *reps_pool, |
| svn_boolean_t at_root, |
| apr_pool_t *pool) |
| { |
| node_revision_t *noderev; |
| apr_off_t my_offset; |
| char my_node_id_buf[MAX_KEY_SIZE + 2]; |
| char my_copy_id_buf[MAX_KEY_SIZE + 2]; |
| const svn_fs_id_t *new_id; |
| const char *node_id, *copy_id, *my_node_id, *my_copy_id; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| *new_id_p = NULL; |
| |
| /* Check to see if this is a transaction node. */ |
| if (! svn_fs_fs__id_txn_id(id)) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs, id, pool)); |
| |
| if (noderev->kind == svn_node_dir) |
| { |
| apr_pool_t *subpool; |
| apr_hash_t *entries, *str_entries; |
| apr_array_header_t *sorted_entries; |
| int i; |
| |
| /* This is a directory. Write out all the children first. */ |
| subpool = svn_pool_create(pool); |
| |
| SVN_ERR(svn_fs_fs__rep_contents_dir(&entries, fs, noderev, pool)); |
| /* For the sake of the repository administrator sort the entries |
| so that the final file is deterministic and repeatable, |
| however the rest of the FSFS code doesn't require any |
| particular order here. */ |
| sorted_entries = svn_sort__hash(entries, svn_sort_compare_items_lexically, |
| pool); |
| for (i = 0; i < sorted_entries->nelts; ++i) |
| { |
| svn_fs_dirent_t *dirent = APR_ARRAY_IDX(sorted_entries, i, |
| svn_sort__item_t).value; |
| |
| svn_pool_clear(subpool); |
| SVN_ERR(write_final_rev(&new_id, file, rev, fs, dirent->id, |
| start_node_id, start_copy_id, initial_offset, |
| reps_to_cache, reps_hash, reps_pool, FALSE, |
| subpool)); |
| if (new_id && (svn_fs_fs__id_rev(new_id) == rev)) |
| dirent->id = svn_fs_fs__id_copy(new_id, pool); |
| } |
| svn_pool_destroy(subpool); |
| |
| if (noderev->data_rep && noderev->data_rep->txn_id) |
| { |
| /* Write out the contents of this directory as a text rep. */ |
| SVN_ERR(unparse_dir_entries(&str_entries, entries, pool)); |
| |
| noderev->data_rep->txn_id = NULL; |
| noderev->data_rep->revision = rev; |
| |
| if (ffd->deltify_directories) |
| SVN_ERR(write_hash_delta_rep(noderev->data_rep, file, |
| str_entries, fs, noderev, NULL, |
| FALSE, pool)); |
| else |
| SVN_ERR(write_hash_rep(noderev->data_rep, file, str_entries, |
| fs, NULL, pool)); |
| } |
| } |
| else |
| { |
| /* This is a file. We should make sure the data rep, if it |
| exists in a "this" state, gets rewritten to our new revision |
| num. */ |
| |
| if (noderev->data_rep && noderev->data_rep->txn_id) |
| { |
| noderev->data_rep->txn_id = NULL; |
| noderev->data_rep->revision = rev; |
| |
| /* See issue 3845. Some unknown mechanism caused the |
| protorev file to get truncated, so check for that |
| here. */ |
| if (noderev->data_rep->offset + noderev->data_rep->size |
| > initial_offset) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Truncated protorev file detected")); |
| } |
| } |
| |
| /* Fix up the property reps. */ |
| if (noderev->prop_rep && noderev->prop_rep->txn_id) |
| { |
| apr_hash_t *proplist; |
| SVN_ERR(svn_fs_fs__get_proplist(&proplist, fs, noderev, pool)); |
| |
| noderev->prop_rep->txn_id = NULL; |
| noderev->prop_rep->revision = rev; |
| |
| if (ffd->deltify_properties) |
| SVN_ERR(write_hash_delta_rep(noderev->prop_rep, file, |
| proplist, fs, noderev, reps_hash, |
| TRUE, pool)); |
| else |
| SVN_ERR(write_hash_rep(noderev->prop_rep, file, proplist, |
| fs, reps_hash, pool)); |
| } |
| |
| |
| /* Convert our temporary ID into a permanent revision one. */ |
| SVN_ERR(get_file_offset(&my_offset, file, pool)); |
| |
| node_id = svn_fs_fs__id_node_id(noderev->id); |
| if (*node_id == '_') |
| { |
| if (ffd->format >= SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT) |
| my_node_id = apr_psprintf(pool, "%s-%ld", node_id + 1, rev); |
| else |
| { |
| svn_fs_fs__add_keys(start_node_id, node_id + 1, my_node_id_buf); |
| my_node_id = my_node_id_buf; |
| } |
| } |
| else |
| my_node_id = node_id; |
| |
| copy_id = svn_fs_fs__id_copy_id(noderev->id); |
| if (*copy_id == '_') |
| { |
| if (ffd->format >= SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT) |
| my_copy_id = apr_psprintf(pool, "%s-%ld", copy_id + 1, rev); |
| else |
| { |
| svn_fs_fs__add_keys(start_copy_id, copy_id + 1, my_copy_id_buf); |
| my_copy_id = my_copy_id_buf; |
| } |
| } |
| else |
| my_copy_id = copy_id; |
| |
| if (noderev->copyroot_rev == SVN_INVALID_REVNUM) |
| noderev->copyroot_rev = rev; |
| |
| new_id = svn_fs_fs__id_rev_create(my_node_id, my_copy_id, rev, my_offset, |
| pool); |
| |
| noderev->id = new_id; |
| |
| if (ffd->rep_sharing_allowed) |
| { |
| /* Save the data representation's hash in the rep cache. */ |
| if ( noderev->data_rep && noderev->kind == svn_node_file |
| && noderev->data_rep->revision == rev) |
| { |
| SVN_ERR_ASSERT(reps_to_cache && reps_pool); |
| APR_ARRAY_PUSH(reps_to_cache, representation_t *) |
| = svn_fs_fs__rep_copy(noderev->data_rep, reps_pool); |
| } |
| |
| if (noderev->prop_rep && noderev->prop_rep->revision == rev) |
| { |
| /* Add new property reps to hash and on-disk cache. */ |
| representation_t *copy |
| = svn_fs_fs__rep_copy(noderev->prop_rep, reps_pool); |
| |
| SVN_ERR_ASSERT(reps_to_cache && reps_pool); |
| APR_ARRAY_PUSH(reps_to_cache, representation_t *) = copy; |
| |
| apr_hash_set(reps_hash, |
| copy->sha1_checksum->digest, |
| APR_SHA1_DIGESTSIZE, |
| copy); |
| } |
| } |
| |
| /* don't serialize SHA1 for dirs to disk (waste of space) */ |
| if (noderev->data_rep && noderev->kind == svn_node_dir) |
| noderev->data_rep->sha1_checksum = NULL; |
| |
| /* don't serialize SHA1 for props to disk (waste of space) */ |
| if (noderev->prop_rep) |
| noderev->prop_rep->sha1_checksum = NULL; |
| |
| /* Workaround issue #4031: is-fresh-txn-root in revision files. */ |
| noderev->is_fresh_txn_root = FALSE; |
| |
| /* Write out our new node-revision. */ |
| if (at_root) |
| SVN_ERR(validate_root_noderev(fs, noderev, rev, pool)); |
| |
| SVN_ERR(svn_fs_fs__write_noderev(svn_stream_from_aprfile2(file, TRUE, pool), |
| noderev, ffd->format, |
| svn_fs_fs__fs_supports_mergeinfo(fs), |
| pool)); |
| |
| /* Return our ID that references the revision file. */ |
| *new_id_p = noderev->id; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write the changed path info from transaction TXN_ID in filesystem |
| FS to the permanent rev-file FILE. *OFFSET_P is set the to offset |
| in the file of the beginning of this information. Perform |
| temporary allocations in POOL. */ |
| static svn_error_t * |
| write_final_changed_path_info(apr_off_t *offset_p, |
| apr_file_t *file, |
| svn_fs_t *fs, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *changed_paths; |
| apr_off_t offset; |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_boolean_t include_node_kinds = |
| ffd->format >= SVN_FS_FS__MIN_KIND_IN_CHANGED_FORMAT; |
| apr_array_header_t *sorted_changed_paths; |
| int i; |
| |
| SVN_ERR(get_file_offset(&offset, file, pool)); |
| |
| SVN_ERR(svn_fs_fs__txn_changes_fetch(&changed_paths, fs, txn_id, pool)); |
| /* For the sake of the repository administrator sort the changes so |
| that the final file is deterministic and repeatable, however the |
| rest of the FSFS code doesn't require any particular order here. */ |
| sorted_changed_paths = svn_sort__hash(changed_paths, |
| svn_sort_compare_items_lexically, pool); |
| |
| /* Iterate through the changed paths one at a time, and convert the |
| temporary node-id into a permanent one for each change entry. */ |
| for (i = 0; i < sorted_changed_paths->nelts; ++i) |
| { |
| node_revision_t *noderev; |
| const svn_fs_id_t *id; |
| svn_fs_path_change2_t *change; |
| const char *path; |
| |
| svn_pool_clear(iterpool); |
| |
| change = APR_ARRAY_IDX(sorted_changed_paths, i, svn_sort__item_t).value; |
| path = APR_ARRAY_IDX(sorted_changed_paths, i, svn_sort__item_t).key; |
| |
| id = change->node_rev_id; |
| |
| /* If this was a delete of a mutable node, then it is OK to |
| leave the change entry pointing to the non-existent temporary |
| node, since it will never be used. */ |
| if ((change->change_kind != svn_fs_path_change_delete) && |
| (! svn_fs_fs__id_txn_id(id))) |
| { |
| SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs, id, iterpool)); |
| |
| /* noderev has the permanent node-id at this point, so we just |
| substitute it for the temporary one. */ |
| change->node_rev_id = noderev->id; |
| } |
| |
| /* Write out the new entry into the final rev-file. */ |
| SVN_ERR(write_change_entry(file, path, change, include_node_kinds, |
| iterpool)); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| *offset_p = offset; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Atomically update the 'current' file to hold the specifed REV, |
| NEXT_NODE_ID, and NEXT_COPY_ID. (The two next-ID parameters are |
| ignored and may be NULL if the FS format does not use them.) |
| Perform temporary allocations in POOL. */ |
| static svn_error_t * |
| write_current(svn_fs_t *fs, svn_revnum_t rev, const char *next_node_id, |
| const char *next_copy_id, apr_pool_t *pool) |
| { |
| char *buf; |
| const char *tmp_name, *name; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| /* Now we can just write out this line. */ |
| if (ffd->format >= SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT) |
| buf = apr_psprintf(pool, "%ld\n", rev); |
| else |
| buf = apr_psprintf(pool, "%ld %s %s\n", rev, next_node_id, next_copy_id); |
| |
| name = svn_fs_fs__path_current(fs, pool); |
| SVN_ERR(svn_io_write_unique(&tmp_name, |
| svn_dirent_dirname(name, pool), |
| buf, strlen(buf), |
| svn_io_file_del_none, pool)); |
| |
| return move_into_place(tmp_name, name, name, pool); |
| } |
| |
| /* Open a new svn_fs_t handle to FS, set that handle's concept of "current |
| youngest revision" to NEW_REV, and call svn_fs_fs__verify_root() on |
| NEW_REV's revision root. |
| |
| Intended to be called as the very last step in a commit before 'current' |
| is bumped. This implies that we are holding the write lock. */ |
| static svn_error_t * |
| verify_as_revision_before_current_plus_plus(svn_fs_t *fs, |
| svn_revnum_t new_rev, |
| apr_pool_t *pool) |
| { |
| #ifdef SVN_DEBUG |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_fs_t *ft; /* fs++ == ft */ |
| svn_fs_root_t *root; |
| fs_fs_data_t *ft_ffd; |
| apr_hash_t *fs_config; |
| |
| SVN_ERR_ASSERT(ffd->svn_fs_open_); |
| |
| /* make sure FT does not simply return data cached by other instances |
| * but actually retrieves it from disk at least once. |
| */ |
| fs_config = apr_hash_make(pool); |
| svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_CACHE_NS, |
| svn_uuid_generate(pool)); |
| SVN_ERR(ffd->svn_fs_open_(&ft, fs->path, |
| fs_config, |
| pool)); |
| ft_ffd = ft->fsap_data; |
| /* Don't let FT consult rep-cache.db, either. */ |
| ft_ffd->rep_sharing_allowed = FALSE; |
| |
| /* Time travel! */ |
| ft_ffd->youngest_rev_cache = new_rev; |
| |
| SVN_ERR(svn_fs_fs__revision_root(&root, ft, new_rev, pool)); |
| SVN_ERR_ASSERT(root->is_txn_root == FALSE && root->rev == new_rev); |
| SVN_ERR_ASSERT(ft_ffd->youngest_rev_cache == new_rev); |
| SVN_ERR(svn_fs_fs__verify_root(root, pool)); |
| #endif /* SVN_DEBUG */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Update the 'current' file to hold the correct next node and copy_ids |
| from transaction TXN_ID in filesystem FS. The current revision is |
| set to REV. Perform temporary allocations in POOL. */ |
| static svn_error_t * |
| write_final_current(svn_fs_t *fs, |
| const char *txn_id, |
| svn_revnum_t rev, |
| const char *start_node_id, |
| const char *start_copy_id, |
| apr_pool_t *pool) |
| { |
| const char *txn_node_id, *txn_copy_id; |
| char new_node_id[MAX_KEY_SIZE + 2]; |
| char new_copy_id[MAX_KEY_SIZE + 2]; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| if (ffd->format >= SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT) |
| return write_current(fs, rev, NULL, NULL, pool); |
| |
| /* To find the next available ids, we add the id that used to be in |
| the 'current' file, to the next ids from the transaction file. */ |
| SVN_ERR(read_next_ids(&txn_node_id, &txn_copy_id, fs, txn_id, pool)); |
| |
| svn_fs_fs__add_keys(start_node_id, txn_node_id, new_node_id); |
| svn_fs_fs__add_keys(start_copy_id, txn_copy_id, new_copy_id); |
| |
| return write_current(fs, rev, new_node_id, new_copy_id, pool); |
| } |
| |
| /* Verify that the user registed with FS has all the locks necessary to |
| permit all the changes associate with TXN_NAME. |
| The FS write lock is assumed to be held by the caller. */ |
| static svn_error_t * |
| verify_locks(svn_fs_t *fs, |
| const char *txn_name, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *subpool = svn_pool_create(pool); |
| apr_hash_t *changes; |
| apr_hash_index_t *hi; |
| apr_array_header_t *changed_paths; |
| svn_stringbuf_t *last_recursed = NULL; |
| int i; |
| |
| /* Fetch the changes for this transaction. */ |
| SVN_ERR(svn_fs_fs__txn_changes_fetch(&changes, fs, txn_name, pool)); |
| |
| /* Make an array of the changed paths, and sort them depth-first-ily. */ |
| changed_paths = apr_array_make(pool, apr_hash_count(changes) + 1, |
| sizeof(const char *)); |
| for (hi = apr_hash_first(pool, changes); hi; hi = apr_hash_next(hi)) |
| APR_ARRAY_PUSH(changed_paths, const char *) = svn__apr_hash_index_key(hi); |
| qsort(changed_paths->elts, changed_paths->nelts, |
| changed_paths->elt_size, svn_sort_compare_paths); |
| |
| /* Now, traverse the array of changed paths, verify locks. Note |
| that if we need to do a recursive verification a path, we'll skip |
| over children of that path when we get to them. */ |
| for (i = 0; i < changed_paths->nelts; i++) |
| { |
| const char *path; |
| svn_fs_path_change2_t *change; |
| svn_boolean_t recurse = TRUE; |
| |
| svn_pool_clear(subpool); |
| path = APR_ARRAY_IDX(changed_paths, i, const char *); |
| |
| /* If this path has already been verified as part of a recursive |
| check of one of its parents, no need to do it again. */ |
| if (last_recursed |
| && svn_dirent_is_child(last_recursed->data, path, subpool)) |
| continue; |
| |
| /* Fetch the change associated with our path. */ |
| change = svn_hash_gets(changes, path); |
| |
| /* What does it mean to succeed at lock verification for a given |
| path? For an existing file or directory getting modified |
| (text, props), it means we hold the lock on the file or |
| directory. For paths being added or removed, we need to hold |
| the locks for that path and any children of that path. |
| |
| WHEW! We have no reliable way to determine the node kind |
| of deleted items, but fortunately we are going to do a |
| recursive check on deleted paths regardless of their kind. */ |
| if (change->change_kind == svn_fs_path_change_modify) |
| recurse = FALSE; |
| SVN_ERR(svn_fs_fs__allow_locked_operation(path, fs, recurse, TRUE, |
| subpool)); |
| |
| /* If we just did a recursive check, remember the path we |
| checked (so children can be skipped). */ |
| if (recurse) |
| { |
| if (! last_recursed) |
| last_recursed = svn_stringbuf_create(path, pool); |
| else |
| svn_stringbuf_set(last_recursed, path); |
| } |
| } |
| svn_pool_destroy(subpool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Baton used for commit_body below. */ |
| struct commit_baton { |
| svn_revnum_t *new_rev_p; |
| svn_fs_t *fs; |
| svn_fs_txn_t *txn; |
| apr_array_header_t *reps_to_cache; |
| apr_hash_t *reps_hash; |
| apr_pool_t *reps_pool; |
| }; |
| |
| /* The work-horse for svn_fs_fs__commit, called with the FS write lock. |
| This implements the svn_fs_fs__with_write_lock() 'body' callback |
| type. BATON is a 'struct commit_baton *'. */ |
| static svn_error_t * |
| commit_body(void *baton, apr_pool_t *pool) |
| { |
| struct commit_baton *cb = baton; |
| fs_fs_data_t *ffd = cb->fs->fsap_data; |
| const char *old_rev_filename, *rev_filename, *proto_filename; |
| const char *revprop_filename, *final_revprop; |
| const svn_fs_id_t *root_id, *new_root_id; |
| const char *start_node_id = NULL, *start_copy_id = NULL; |
| svn_revnum_t old_rev, new_rev; |
| apr_file_t *proto_file; |
| void *proto_file_lockcookie; |
| apr_off_t initial_offset, changed_path_offset; |
| char *buf; |
| apr_hash_t *txnprops; |
| apr_array_header_t *txnprop_list; |
| svn_prop_t prop; |
| svn_string_t date; |
| |
| /* Get the current youngest revision. */ |
| SVN_ERR(svn_fs_fs__youngest_rev(&old_rev, cb->fs, pool)); |
| |
| /* Check to make sure this transaction is based off the most recent |
| revision. */ |
| if (cb->txn->base_rev != old_rev) |
| return svn_error_create(SVN_ERR_FS_TXN_OUT_OF_DATE, NULL, |
| _("Transaction out of date")); |
| |
| /* Locks may have been added (or stolen) between the calling of |
| previous svn_fs.h functions and svn_fs_commit_txn(), so we need |
| to re-examine every changed-path in the txn and re-verify all |
| discovered locks. */ |
| SVN_ERR(verify_locks(cb->fs, cb->txn->id, pool)); |
| |
| /* Get the next node_id and copy_id to use. */ |
| if (ffd->format < SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT) |
| SVN_ERR(get_next_revision_ids(&start_node_id, &start_copy_id, cb->fs, |
| pool)); |
| |
| /* We are going to be one better than this puny old revision. */ |
| new_rev = old_rev + 1; |
| |
| /* Get a write handle on the proto revision file. */ |
| SVN_ERR(get_writable_proto_rev(&proto_file, &proto_file_lockcookie, |
| cb->fs, cb->txn->id, pool)); |
| SVN_ERR(get_file_offset(&initial_offset, proto_file, pool)); |
| |
| /* Write out all the node-revisions and directory contents. */ |
| root_id = svn_fs_fs__id_txn_create("0", "0", cb->txn->id, pool); |
| SVN_ERR(write_final_rev(&new_root_id, proto_file, new_rev, cb->fs, root_id, |
| start_node_id, start_copy_id, initial_offset, |
| cb->reps_to_cache, cb->reps_hash, cb->reps_pool, |
| TRUE, pool)); |
| |
| /* Write the changed-path information. */ |
| SVN_ERR(write_final_changed_path_info(&changed_path_offset, proto_file, |
| cb->fs, cb->txn->id, pool)); |
| |
| /* Write the final line. */ |
| buf = apr_psprintf(pool, "\n%" APR_OFF_T_FMT " %" APR_OFF_T_FMT "\n", |
| svn_fs_fs__id_offset(new_root_id), |
| changed_path_offset); |
| SVN_ERR(svn_io_file_write_full(proto_file, buf, strlen(buf), NULL, |
| pool)); |
| SVN_ERR(svn_io_file_flush_to_disk(proto_file, pool)); |
| SVN_ERR(svn_io_file_close(proto_file, pool)); |
| |
| /* We don't unlock the prototype revision file immediately to avoid a |
| race with another caller writing to the prototype revision file |
| before we commit it. */ |
| |
| /* Remove any temporary txn props representing 'flags'. */ |
| SVN_ERR(svn_fs_fs__txn_proplist(&txnprops, cb->txn, pool)); |
| txnprop_list = apr_array_make(pool, 3, sizeof(svn_prop_t)); |
| prop.value = NULL; |
| |
| if (svn_hash_gets(txnprops, SVN_FS__PROP_TXN_CHECK_OOD)) |
| { |
| prop.name = SVN_FS__PROP_TXN_CHECK_OOD; |
| APR_ARRAY_PUSH(txnprop_list, svn_prop_t) = prop; |
| } |
| |
| if (svn_hash_gets(txnprops, SVN_FS__PROP_TXN_CHECK_LOCKS)) |
| { |
| prop.name = SVN_FS__PROP_TXN_CHECK_LOCKS; |
| APR_ARRAY_PUSH(txnprop_list, svn_prop_t) = prop; |
| } |
| |
| if (! apr_is_empty_array(txnprop_list)) |
| SVN_ERR(svn_fs_fs__change_txn_props(cb->txn, txnprop_list, pool)); |
| |
| /* Create the shard for the rev and revprop file, if we're sharding and |
| this is the first revision of a new shard. We don't care if this |
| fails because the shard already existed for some reason. */ |
| if (ffd->max_files_per_dir && new_rev % ffd->max_files_per_dir == 0) |
| { |
| /* Create the revs shard. */ |
| { |
| const char *new_dir = path_rev_shard(cb->fs, new_rev, pool); |
| svn_error_t *err = svn_io_dir_make(new_dir, APR_OS_DEFAULT, pool); |
| if (err && !APR_STATUS_IS_EEXIST(err->apr_err)) |
| return svn_error_trace(err); |
| svn_error_clear(err); |
| SVN_ERR(svn_io_copy_perms(svn_dirent_join(cb->fs->path, |
| PATH_REVS_DIR, |
| pool), |
| new_dir, pool)); |
| } |
| |
| /* Create the revprops shard. */ |
| SVN_ERR_ASSERT(! is_packed_revprop(cb->fs, new_rev)); |
| { |
| const char *new_dir = path_revprops_shard(cb->fs, new_rev, pool); |
| svn_error_t *err = svn_io_dir_make(new_dir, APR_OS_DEFAULT, pool); |
| if (err && !APR_STATUS_IS_EEXIST(err->apr_err)) |
| return svn_error_trace(err); |
| svn_error_clear(err); |
| SVN_ERR(svn_io_copy_perms(svn_dirent_join(cb->fs->path, |
| PATH_REVPROPS_DIR, |
| pool), |
| new_dir, pool)); |
| } |
| } |
| |
| /* Move the finished rev file into place. */ |
| SVN_ERR(svn_fs_fs__path_rev_absolute(&old_rev_filename, |
| cb->fs, old_rev, pool)); |
| rev_filename = path_rev(cb->fs, new_rev, pool); |
| proto_filename = path_txn_proto_rev(cb->fs, cb->txn->id, pool); |
| SVN_ERR(move_into_place(proto_filename, rev_filename, old_rev_filename, |
| pool)); |
| |
| /* Now that we've moved the prototype revision file out of the way, |
| we can unlock it (since further attempts to write to the file |
| will fail as it no longer exists). We must do this so that we can |
| remove the transaction directory later. */ |
| SVN_ERR(unlock_proto_rev(cb->fs, cb->txn->id, proto_file_lockcookie, pool)); |
| |
| /* Update commit time to ensure that svn:date revprops remain ordered. */ |
| date.data = svn_time_to_cstring(apr_time_now(), pool); |
| date.len = strlen(date.data); |
| |
| SVN_ERR(svn_fs_fs__change_txn_prop(cb->txn, SVN_PROP_REVISION_DATE, |
| &date, pool)); |
| |
| /* Move the revprops file into place. */ |
| SVN_ERR_ASSERT(! is_packed_revprop(cb->fs, new_rev)); |
| revprop_filename = path_txn_props(cb->fs, cb->txn->id, pool); |
| final_revprop = path_revprops(cb->fs, new_rev, pool); |
| SVN_ERR(move_into_place(revprop_filename, final_revprop, |
| old_rev_filename, pool)); |
| |
| /* Update the 'current' file. */ |
| SVN_ERR(verify_as_revision_before_current_plus_plus(cb->fs, new_rev, pool)); |
| SVN_ERR(write_final_current(cb->fs, cb->txn->id, new_rev, start_node_id, |
| start_copy_id, pool)); |
| |
| /* At this point the new revision is committed and globally visible |
| so let the caller know it succeeded by giving it the new revision |
| number, which fulfills svn_fs_commit_txn() contract. Any errors |
| after this point do not change the fact that a new revision was |
| created. */ |
| *cb->new_rev_p = new_rev; |
| |
| ffd->youngest_rev_cache = new_rev; |
| |
| /* Remove this transaction directory. */ |
| SVN_ERR(svn_fs_fs__purge_txn(cb->fs, cb->txn->id, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Add the representations in REPS_TO_CACHE (an array of representation_t *) |
| * to the rep-cache database of FS. */ |
| static svn_error_t * |
| write_reps_to_cache(svn_fs_t *fs, |
| const apr_array_header_t *reps_to_cache, |
| apr_pool_t *scratch_pool) |
| { |
| int i; |
| |
| for (i = 0; i < reps_to_cache->nelts; i++) |
| { |
| representation_t *rep = APR_ARRAY_IDX(reps_to_cache, i, representation_t *); |
| |
| /* FALSE because we don't care if another parallel commit happened to |
| * collide with us. (Non-parallel collisions will not be detected.) */ |
| SVN_ERR(svn_fs_fs__set_rep_reference(fs, rep, FALSE, scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__commit(svn_revnum_t *new_rev_p, |
| svn_fs_t *fs, |
| svn_fs_txn_t *txn, |
| apr_pool_t *pool) |
| { |
| struct commit_baton cb; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| cb.new_rev_p = new_rev_p; |
| cb.fs = fs; |
| cb.txn = txn; |
| |
| if (ffd->rep_sharing_allowed) |
| { |
| cb.reps_to_cache = apr_array_make(pool, 5, sizeof(representation_t *)); |
| cb.reps_hash = apr_hash_make(pool); |
| cb.reps_pool = pool; |
| } |
| else |
| { |
| cb.reps_to_cache = NULL; |
| cb.reps_hash = NULL; |
| cb.reps_pool = NULL; |
| } |
| |
| SVN_ERR(svn_fs_fs__with_write_lock(fs, commit_body, &cb, pool)); |
| |
| /* At this point, *NEW_REV_P has been set, so errors below won't affect |
| the success of the commit. (See svn_fs_commit_txn().) */ |
| |
| if (ffd->rep_sharing_allowed) |
| { |
| SVN_ERR(svn_fs_fs__open_rep_cache(fs, pool)); |
| |
| /* Write new entries to the rep-sharing database. |
| * |
| * We use an sqlite transaction to speed things up; |
| * see <http://www.sqlite.org/faq.html#q19>. |
| */ |
| SVN_SQLITE__WITH_TXN( |
| write_reps_to_cache(fs, cb.reps_to_cache, pool), |
| ffd->rep_cache_db); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_fs_fs__reserve_copy_id(const char **copy_id_p, |
| svn_fs_t *fs, |
| const char *txn_id, |
| apr_pool_t *pool) |
| { |
| const char *cur_node_id, *cur_copy_id; |
| char *copy_id; |
| apr_size_t len; |
| |
| /* First read in the current next-ids file. */ |
| SVN_ERR(read_next_ids(&cur_node_id, &cur_copy_id, fs, txn_id, pool)); |
| |
| copy_id = apr_pcalloc(pool, strlen(cur_copy_id) + 2); |
| |
| len = strlen(cur_copy_id); |
| svn_fs_fs__next_key(cur_copy_id, &len, copy_id); |
| |
| SVN_ERR(write_next_ids(fs, txn_id, cur_node_id, copy_id, pool)); |
| |
| *copy_id_p = apr_pstrcat(pool, "_", cur_copy_id, (char *)NULL); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write out the zeroth revision for filesystem FS. */ |
| static svn_error_t * |
| write_revision_zero(svn_fs_t *fs) |
| { |
| const char *path_revision_zero = path_rev(fs, 0, fs->pool); |
| apr_hash_t *proplist; |
| svn_string_t date; |
| |
| /* Write out a rev file for revision 0. */ |
| SVN_ERR(svn_io_file_create(path_revision_zero, |
| "PLAIN\nEND\nENDREP\n" |
| "id: 0.0.r0/17\n" |
| "type: dir\n" |
| "count: 0\n" |
| "text: 0 0 4 4 " |
| "2d2977d1c96f487abe4a1e202dd03b4e\n" |
| "cpath: /\n" |
| "\n\n17 107\n", fs->pool)); |
| SVN_ERR(svn_io_set_file_read_only(path_revision_zero, FALSE, fs->pool)); |
| |
| /* Set a date on revision 0. */ |
| date.data = svn_time_to_cstring(apr_time_now(), fs->pool); |
| date.len = strlen(date.data); |
| proplist = apr_hash_make(fs->pool); |
| svn_hash_sets(proplist, SVN_PROP_REVISION_DATE, &date); |
| return set_revision_proplist(fs, 0, proplist, fs->pool); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__create(svn_fs_t *fs, |
| const char *path, |
| apr_pool_t *pool) |
| { |
| int format = SVN_FS_FS__FORMAT_NUMBER; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| |
| fs->path = apr_pstrdup(pool, path); |
| /* See if compatibility with older versions was explicitly requested. */ |
| if (fs->config) |
| { |
| if (svn_hash_gets(fs->config, SVN_FS_CONFIG_PRE_1_4_COMPATIBLE)) |
| format = 1; |
| else if (svn_hash_gets(fs->config, SVN_FS_CONFIG_PRE_1_5_COMPATIBLE)) |
| format = 2; |
| else if (svn_hash_gets(fs->config, SVN_FS_CONFIG_PRE_1_6_COMPATIBLE)) |
| format = 3; |
| else if (svn_hash_gets(fs->config, SVN_FS_CONFIG_PRE_1_8_COMPATIBLE)) |
| format = 4; |
| } |
| ffd->format = format; |
| |
| /* Override the default linear layout if this is a new-enough format. */ |
| if (format >= SVN_FS_FS__MIN_LAYOUT_FORMAT_OPTION_FORMAT) |
| ffd->max_files_per_dir = SVN_FS_FS_DEFAULT_MAX_FILES_PER_DIR; |
| |
| /* Create the revision data directories. */ |
| if (ffd->max_files_per_dir) |
| SVN_ERR(svn_io_make_dir_recursively(path_rev_shard(fs, 0, pool), pool)); |
| else |
| SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(path, PATH_REVS_DIR, |
| pool), |
| pool)); |
| |
| /* Create the revprops directory. */ |
| if (ffd->max_files_per_dir) |
| SVN_ERR(svn_io_make_dir_recursively(path_revprops_shard(fs, 0, pool), |
| pool)); |
| else |
| SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(path, |
| PATH_REVPROPS_DIR, |
| pool), |
| pool)); |
| |
| /* Create the transaction directory. */ |
| SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(path, PATH_TXNS_DIR, |
| pool), |
| pool)); |
| |
| /* Create the protorevs directory. */ |
| if (format >= SVN_FS_FS__MIN_PROTOREVS_DIR_FORMAT) |
| SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(path, PATH_TXN_PROTOS_DIR, |
| pool), |
| pool)); |
| |
| /* Create the 'current' file. */ |
| SVN_ERR(svn_io_file_create(svn_fs_fs__path_current(fs, pool), |
| (format >= SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT |
| ? "0\n" : "0 1 1\n"), |
| pool)); |
| SVN_ERR(svn_io_file_create(path_lock(fs, pool), "", pool)); |
| SVN_ERR(svn_fs_fs__set_uuid(fs, NULL, pool)); |
| |
| SVN_ERR(write_revision_zero(fs)); |
| |
| /* Create the fsfs.conf file if supported. Older server versions would |
| simply ignore the file but that might result in a different behavior |
| than with the later releases. Also, hotcopy would ignore, i.e. not |
| copy, a fsfs.conf with old formats. */ |
| if (ffd->format >= SVN_FS_FS__MIN_CONFIG_FILE) |
| SVN_ERR(write_config(fs, pool)); |
| |
| SVN_ERR(read_config(ffd, fs->path, pool)); |
| |
| /* Create the min unpacked rev file. */ |
| if (ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) |
| SVN_ERR(svn_io_file_create(path_min_unpacked_rev(fs, pool), "0\n", pool)); |
| |
| /* Create the txn-current file if the repository supports |
| the transaction sequence file. */ |
| if (format >= SVN_FS_FS__MIN_TXN_CURRENT_FORMAT) |
| { |
| SVN_ERR(svn_io_file_create(path_txn_current(fs, pool), |
| "0\n", pool)); |
| SVN_ERR(svn_io_file_create(path_txn_current_lock(fs, pool), |
| "", pool)); |
| } |
| |
| /* This filesystem is ready. Stamp it with a format number. */ |
| SVN_ERR(write_format(path_format(fs, pool), |
| ffd->format, ffd->max_files_per_dir, FALSE, pool)); |
| |
| ffd->youngest_rev_cache = 0; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Part of the recovery procedure. Return the largest revision *REV in |
| filesystem FS. Use POOL for temporary allocation. */ |
| static svn_error_t * |
| recover_get_largest_revision(svn_fs_t *fs, svn_revnum_t *rev, apr_pool_t *pool) |
| { |
| /* Discovering the largest revision in the filesystem would be an |
| expensive operation if we did a readdir() or searched linearly, |
| so we'll do a form of binary search. left is a revision that we |
| know exists, right a revision that we know does not exist. */ |
| apr_pool_t *iterpool; |
| svn_revnum_t left, right = 1; |
| |
| iterpool = svn_pool_create(pool); |
| /* Keep doubling right, until we find a revision that doesn't exist. */ |
| while (1) |
| { |
| svn_error_t *err; |
| apr_file_t *file; |
| |
| err = open_pack_or_rev_file(&file, fs, right, iterpool); |
| svn_pool_clear(iterpool); |
| |
| if (err && err->apr_err == SVN_ERR_FS_NO_SUCH_REVISION) |
| { |
| svn_error_clear(err); |
| break; |
| } |
| else |
| SVN_ERR(err); |
| |
| right <<= 1; |
| } |
| |
| left = right >> 1; |
| |
| /* We know that left exists and right doesn't. Do a normal bsearch to find |
| the last revision. */ |
| while (left + 1 < right) |
| { |
| svn_revnum_t probe = left + ((right - left) / 2); |
| svn_error_t *err; |
| apr_file_t *file; |
| |
| err = open_pack_or_rev_file(&file, fs, probe, iterpool); |
| svn_pool_clear(iterpool); |
| |
| if (err && err->apr_err == SVN_ERR_FS_NO_SUCH_REVISION) |
| { |
| svn_error_clear(err); |
| right = probe; |
| } |
| else |
| { |
| SVN_ERR(err); |
| left = probe; |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| /* left is now the largest revision that exists. */ |
| *rev = left; |
| return SVN_NO_ERROR; |
| } |
| |
| /* A baton for reading a fixed amount from an open file. For |
| recover_find_max_ids() below. */ |
| struct recover_read_from_file_baton |
| { |
| apr_file_t *file; |
| apr_pool_t *pool; |
| apr_off_t remaining; |
| }; |
| |
| /* A stream read handler used by recover_find_max_ids() below. |
| Read and return at most BATON->REMAINING bytes from the stream, |
| returning nothing after that to indicate EOF. */ |
| static svn_error_t * |
| read_handler_recover(void *baton, char *buffer, apr_size_t *len) |
| { |
| struct recover_read_from_file_baton *b = baton; |
| svn_filesize_t bytes_to_read = *len; |
| |
| if (b->remaining == 0) |
| { |
| /* Return a successful read of zero bytes to signal EOF. */ |
| *len = 0; |
| return SVN_NO_ERROR; |
| } |
| |
| if (bytes_to_read > b->remaining) |
| bytes_to_read = b->remaining; |
| b->remaining -= bytes_to_read; |
| |
| return svn_io_file_read_full2(b->file, buffer, (apr_size_t) bytes_to_read, |
| len, NULL, b->pool); |
| } |
| |
| /* Part of the recovery procedure. Read the directory noderev at offset |
| OFFSET of file REV_FILE (the revision file of revision REV of |
| filesystem FS), and set MAX_NODE_ID and MAX_COPY_ID to be the node-id |
| and copy-id of that node, if greater than the current value stored |
| in either. Recurse into any child directories that were modified in |
| this revision. |
| |
| MAX_NODE_ID and MAX_COPY_ID must be arrays of at least MAX_KEY_SIZE. |
| |
| Perform temporary allocation in POOL. */ |
| static svn_error_t * |
| recover_find_max_ids(svn_fs_t *fs, svn_revnum_t rev, |
| apr_file_t *rev_file, apr_off_t offset, |
| char *max_node_id, char *max_copy_id, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *headers; |
| char *value; |
| representation_t *data_rep; |
| struct rep_args *ra; |
| struct recover_read_from_file_baton baton; |
| svn_stream_t *stream; |
| apr_hash_t *entries; |
| apr_hash_index_t *hi; |
| apr_pool_t *iterpool; |
| |
| SVN_ERR(svn_io_file_seek(rev_file, APR_SET, &offset, pool)); |
| SVN_ERR(read_header_block(&headers, svn_stream_from_aprfile2(rev_file, TRUE, |
| pool), |
| pool)); |
| |
| /* Check that this is a directory. It should be. */ |
| value = svn_hash_gets(headers, HEADER_TYPE); |
| if (value == NULL || strcmp(value, KIND_DIR) != 0) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Recovery encountered a non-directory node")); |
| |
| /* Get the data location. No data location indicates an empty directory. */ |
| value = svn_hash_gets(headers, HEADER_TEXT); |
| if (!value) |
| return SVN_NO_ERROR; |
| SVN_ERR(read_rep_offsets(&data_rep, value, NULL, FALSE, pool)); |
| |
| /* If the directory's data representation wasn't changed in this revision, |
| we've already scanned the directory's contents for noderevs, so we don't |
| need to again. This will occur if a property is changed on a directory |
| without changing the directory's contents. */ |
| if (data_rep->revision != rev) |
| return SVN_NO_ERROR; |
| |
| /* We could use get_dir_contents(), but this is much cheaper. It does |
| rely on directory entries being stored as PLAIN reps, though. */ |
| offset = data_rep->offset; |
| SVN_ERR(svn_io_file_seek(rev_file, APR_SET, &offset, pool)); |
| SVN_ERR(read_rep_line(&ra, rev_file, pool)); |
| if (ra->is_delta) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Recovery encountered a deltified directory " |
| "representation")); |
| |
| /* Now create a stream that's allowed to read only as much data as is |
| stored in the representation. */ |
| baton.file = rev_file; |
| baton.pool = pool; |
| baton.remaining = data_rep->expanded_size |
| ? data_rep->expanded_size |
| : data_rep->size; |
| stream = svn_stream_create(&baton, pool); |
| svn_stream_set_read(stream, read_handler_recover); |
| |
| /* Now read the entries from that stream. */ |
| entries = apr_hash_make(pool); |
| SVN_ERR(svn_hash_read2(entries, stream, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_stream_close(stream)); |
| |
| /* Now check each of the entries in our directory to find new node and |
| copy ids, and recurse into new subdirectories. */ |
| iterpool = svn_pool_create(pool); |
| for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) |
| { |
| char *str_val; |
| char *str; |
| svn_node_kind_t kind; |
| svn_fs_id_t *id; |
| const char *node_id, *copy_id; |
| apr_off_t child_dir_offset; |
| const svn_string_t *path = svn__apr_hash_index_val(hi); |
| |
| svn_pool_clear(iterpool); |
| |
| str_val = apr_pstrdup(iterpool, path->data); |
| |
| str = svn_cstring_tokenize(" ", &str_val); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Directory entry corrupt")); |
| |
| if (strcmp(str, KIND_FILE) == 0) |
| kind = svn_node_file; |
| else if (strcmp(str, KIND_DIR) == 0) |
| kind = svn_node_dir; |
| else |
| { |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Directory entry corrupt")); |
| } |
| |
| str = svn_cstring_tokenize(" ", &str_val); |
| if (str == NULL) |
| return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, |
| _("Directory entry corrupt")); |
| |
| id = svn_fs_fs__id_parse(str, strlen(str), iterpool); |
| |
| if (svn_fs_fs__id_rev(id) != rev) |
| { |
| /* If the node wasn't modified in this revision, we've already |
| checked the node and copy id. */ |
| continue; |
| } |
| |
| node_id = svn_fs_fs__id_node_id(id); |
| copy_id = svn_fs_fs__id_copy_id(id); |
| |
| if (svn_fs_fs__key_compare(node_id, max_node_id) > 0) |
| { |
| SVN_ERR_ASSERT(strlen(node_id) < MAX_KEY_SIZE); |
| apr_cpystrn(max_node_id, node_id, MAX_KEY_SIZE); |
| } |
| if (svn_fs_fs__key_compare(copy_id, max_copy_id) > 0) |
| { |
| SVN_ERR_ASSERT(strlen(copy_id) < MAX_KEY_SIZE); |
| apr_cpystrn(max_copy_id, copy_id, MAX_KEY_SIZE); |
| } |
| |
| if (kind == svn_node_file) |
| continue; |
| |
| child_dir_offset = svn_fs_fs__id_offset(id); |
| SVN_ERR(recover_find_max_ids(fs, rev, rev_file, child_dir_offset, |
| max_node_id, max_copy_id, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return TRUE, if for REVISION in FS, we can find the revprop pack file. |
| * Use POOL for temporary allocations. |
| * Set *MISSING, if the reason is a missing manifest or pack file. |
| */ |
| static svn_boolean_t |
| packed_revprop_available(svn_boolean_t *missing, |
| svn_fs_t *fs, |
| svn_revnum_t revision, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_stringbuf_t *content = NULL; |
| |
| /* try to read the manifest file */ |
| const char *folder = path_revprops_pack_shard(fs, revision, pool); |
| const char *manifest_path = svn_dirent_join(folder, PATH_MANIFEST, pool); |
| |
| svn_error_t *err = try_stringbuf_from_file(&content, |
| missing, |
| manifest_path, |
| FALSE, |
| pool); |
| |
| /* if the manifest cannot be read, consider the pack files inaccessible |
| * even if the file itself exists. */ |
| if (err) |
| { |
| svn_error_clear(err); |
| return FALSE; |
| } |
| |
| if (*missing) |
| return FALSE; |
| |
| /* parse manifest content until we find the entry for REVISION. |
| * Revision 0 is never packed. */ |
| revision = revision < ffd->max_files_per_dir |
| ? revision - 1 |
| : revision % ffd->max_files_per_dir; |
| while (content->data) |
| { |
| char *next = strchr(content->data, '\n'); |
| if (next) |
| { |
| *next = 0; |
| ++next; |
| } |
| |
| if (revision-- == 0) |
| { |
| /* the respective pack file must exist (and be a file) */ |
| svn_node_kind_t kind; |
| err = svn_io_check_path(svn_dirent_join(folder, content->data, |
| pool), |
| &kind, pool); |
| if (err) |
| { |
| svn_error_clear(err); |
| return FALSE; |
| } |
| |
| *missing = kind == svn_node_none; |
| return kind == svn_node_file; |
| } |
| |
| content->data = next; |
| } |
| |
| return FALSE; |
| } |
| |
| /* Baton used for recover_body below. */ |
| struct recover_baton { |
| svn_fs_t *fs; |
| svn_cancel_func_t cancel_func; |
| void *cancel_baton; |
| }; |
| |
| /* The work-horse for svn_fs_fs__recover, called with the FS |
| write lock. This implements the svn_fs_fs__with_write_lock() |
| 'body' callback type. BATON is a 'struct recover_baton *'. */ |
| static svn_error_t * |
| recover_body(void *baton, apr_pool_t *pool) |
| { |
| struct recover_baton *b = baton; |
| svn_fs_t *fs = b->fs; |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_revnum_t max_rev; |
| char next_node_id_buf[MAX_KEY_SIZE], next_copy_id_buf[MAX_KEY_SIZE]; |
| char *next_node_id = NULL, *next_copy_id = NULL; |
| svn_revnum_t youngest_rev; |
| svn_node_kind_t youngest_revprops_kind; |
| |
| /* Lose potentially corrupted data in temp files */ |
| SVN_ERR(cleanup_revprop_namespace(fs)); |
| |
| /* We need to know the largest revision in the filesystem. */ |
| SVN_ERR(recover_get_largest_revision(fs, &max_rev, pool)); |
| |
| /* Get the expected youngest revision */ |
| SVN_ERR(get_youngest(&youngest_rev, fs->path, pool)); |
| |
| /* Policy note: |
| |
| Since the revprops file is written after the revs file, the true |
| maximum available revision is the youngest one for which both are |
| present. That's probably the same as the max_rev we just found, |
| but if it's not, we could, in theory, repeatedly decrement |
| max_rev until we find a revision that has both a revs and |
| revprops file, then write db/current with that. |
| |
| But we choose not to. If a repository is so corrupt that it's |
| missing at least one revprops file, we shouldn't assume that the |
| youngest revision for which both the revs and revprops files are |
| present is healthy. In other words, we're willing to recover |
| from a missing or out-of-date db/current file, because db/current |
| is truly redundant -- it's basically a cache so we don't have to |
| find max_rev each time, albeit a cache with unusual semantics, |
| since it also officially defines when a revision goes live. But |
| if we're missing more than the cache, it's time to back out and |
| let the admin reconstruct things by hand: correctness at that |
| point may depend on external things like checking a commit email |
| list, looking in particular working copies, etc. |
| |
| This policy matches well with a typical naive backup scenario. |
| Say you're rsyncing your FSFS repository nightly to the same |
| location. Once revs and revprops are written, you've got the |
| maximum rev; if the backup should bomb before db/current is |
| written, then db/current could stay arbitrarily out-of-date, but |
| we can still recover. It's a small window, but we might as well |
| do what we can. */ |
| |
| /* Even if db/current were missing, it would be created with 0 by |
| get_youngest(), so this conditional remains valid. */ |
| if (youngest_rev > max_rev) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Expected current rev to be <= %ld " |
| "but found %ld"), max_rev, youngest_rev); |
| |
| /* We only need to search for maximum IDs for old FS formats which |
| se global ID counters. */ |
| if (ffd->format < SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT) |
| { |
| /* Next we need to find the maximum node id and copy id in use across the |
| filesystem. Unfortunately, the only way we can get this information |
| is to scan all the noderevs of all the revisions and keep track as |
| we go along. */ |
| svn_revnum_t rev; |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| char max_node_id[MAX_KEY_SIZE] = "0", max_copy_id[MAX_KEY_SIZE] = "0"; |
| apr_size_t len; |
| |
| for (rev = 0; rev <= max_rev; rev++) |
| { |
| apr_file_t *rev_file; |
| apr_off_t root_offset; |
| |
| svn_pool_clear(iterpool); |
| |
| if (b->cancel_func) |
| SVN_ERR(b->cancel_func(b->cancel_baton)); |
| |
| SVN_ERR(open_pack_or_rev_file(&rev_file, fs, rev, iterpool)); |
| SVN_ERR(get_root_changes_offset(&root_offset, NULL, rev_file, fs, rev, |
| iterpool)); |
| SVN_ERR(recover_find_max_ids(fs, rev, rev_file, root_offset, |
| max_node_id, max_copy_id, iterpool)); |
| SVN_ERR(svn_io_file_close(rev_file, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| /* Now that we finally have the maximum revision, node-id and copy-id, we |
| can bump the two ids to get the next of each. */ |
| len = strlen(max_node_id); |
| svn_fs_fs__next_key(max_node_id, &len, next_node_id_buf); |
| next_node_id = next_node_id_buf; |
| len = strlen(max_copy_id); |
| svn_fs_fs__next_key(max_copy_id, &len, next_copy_id_buf); |
| next_copy_id = next_copy_id_buf; |
| } |
| |
| /* Before setting current, verify that there is a revprops file |
| for the youngest revision. (Issue #2992) */ |
| SVN_ERR(svn_io_check_path(path_revprops(fs, max_rev, pool), |
| &youngest_revprops_kind, pool)); |
| if (youngest_revprops_kind == svn_node_none) |
| { |
| svn_boolean_t missing = TRUE; |
| if (!packed_revprop_available(&missing, fs, max_rev, pool)) |
| { |
| if (missing) |
| { |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Revision %ld has a revs file but no " |
| "revprops file"), |
| max_rev); |
| } |
| else |
| { |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Revision %ld has a revs file but the " |
| "revprops file is inaccessible"), |
| max_rev); |
| } |
| } |
| } |
| else if (youngest_revprops_kind != svn_node_file) |
| { |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Revision %ld has a non-file where its " |
| "revprops file should be"), |
| max_rev); |
| } |
| |
| /* Prune younger-than-(newfound-youngest) revisions from the rep |
| cache if sharing is enabled taking care not to create the cache |
| if it does not exist. */ |
| if (ffd->rep_sharing_allowed) |
| { |
| svn_boolean_t rep_cache_exists; |
| |
| SVN_ERR(svn_fs_fs__exists_rep_cache(&rep_cache_exists, fs, pool)); |
| if (rep_cache_exists) |
| SVN_ERR(svn_fs_fs__del_rep_reference(fs, max_rev, pool)); |
| } |
| |
| /* Now store the discovered youngest revision, and the next IDs if |
| relevant, in a new 'current' file. */ |
| return write_current(fs, max_rev, next_node_id, next_copy_id, pool); |
| } |
| |
| /* This implements the fs_library_vtable_t.recover() API. */ |
| svn_error_t * |
| svn_fs_fs__recover(svn_fs_t *fs, |
| svn_cancel_func_t cancel_func, void *cancel_baton, |
| apr_pool_t *pool) |
| { |
| struct recover_baton b; |
| |
| /* We have no way to take out an exclusive lock in FSFS, so we're |
| restricted as to the types of recovery we can do. Luckily, |
| we just want to recreate the 'current' file, and we can do that just |
| by blocking other writers. */ |
| b.fs = fs; |
| b.cancel_func = cancel_func; |
| b.cancel_baton = cancel_baton; |
| return svn_fs_fs__with_write_lock(fs, recover_body, &b, pool); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__set_uuid(svn_fs_t *fs, |
| const char *uuid, |
| apr_pool_t *pool) |
| { |
| char *my_uuid; |
| apr_size_t my_uuid_len; |
| const char *tmp_path; |
| const char *uuid_path = path_uuid(fs, pool); |
| |
| if (! uuid) |
| uuid = svn_uuid_generate(pool); |
| |
| /* Make sure we have a copy in FS->POOL, and append a newline. */ |
| my_uuid = apr_pstrcat(fs->pool, uuid, "\n", (char *)NULL); |
| my_uuid_len = strlen(my_uuid); |
| |
| SVN_ERR(svn_io_write_unique(&tmp_path, |
| svn_dirent_dirname(uuid_path, pool), |
| my_uuid, my_uuid_len, |
| svn_io_file_del_none, pool)); |
| |
| /* We use the permissions of the 'current' file, because the 'uuid' |
| file does not exist during repository creation. */ |
| SVN_ERR(move_into_place(tmp_path, uuid_path, |
| svn_fs_fs__path_current(fs, pool), pool)); |
| |
| /* Remove the newline we added, and stash the UUID. */ |
| my_uuid[my_uuid_len - 1] = '\0'; |
| fs->uuid = my_uuid; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /** Node origin lazy cache. */ |
| |
| /* If directory PATH does not exist, create it and give it the same |
| permissions as FS_path.*/ |
| svn_error_t * |
| svn_fs_fs__ensure_dir_exists(const char *path, |
| const char *fs_path, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err = svn_io_dir_make(path, APR_OS_DEFAULT, pool); |
| if (err && APR_STATUS_IS_EEXIST(err->apr_err)) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| SVN_ERR(err); |
| |
| /* We successfully created a new directory. Dup the permissions |
| from FS->path. */ |
| return svn_io_copy_perms(fs_path, path, pool); |
| } |
| |
| /* Set *NODE_ORIGINS to a hash mapping 'const char *' node IDs to |
| 'svn_string_t *' node revision IDs. Use POOL for allocations. */ |
| static svn_error_t * |
| get_node_origins_from_file(svn_fs_t *fs, |
| apr_hash_t **node_origins, |
| const char *node_origins_file, |
| apr_pool_t *pool) |
| { |
| apr_file_t *fd; |
| svn_error_t *err; |
| svn_stream_t *stream; |
| |
| *node_origins = NULL; |
| err = svn_io_file_open(&fd, node_origins_file, |
| APR_READ, APR_OS_DEFAULT, pool); |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| SVN_ERR(err); |
| |
| stream = svn_stream_from_aprfile2(fd, FALSE, pool); |
| *node_origins = apr_hash_make(pool); |
| SVN_ERR(svn_hash_read2(*node_origins, stream, SVN_HASH_TERMINATOR, pool)); |
| return svn_stream_close(stream); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__get_node_origin(const svn_fs_id_t **origin_id, |
| svn_fs_t *fs, |
| const char *node_id, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *node_origins; |
| |
| *origin_id = NULL; |
| SVN_ERR(get_node_origins_from_file(fs, &node_origins, |
| path_node_origin(fs, node_id, pool), |
| pool)); |
| if (node_origins) |
| { |
| svn_string_t *origin_id_str = |
| svn_hash_gets(node_origins, node_id); |
| if (origin_id_str) |
| *origin_id = svn_fs_fs__id_parse(origin_id_str->data, |
| origin_id_str->len, pool); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Helper for svn_fs_fs__set_node_origin. Takes a NODE_ID/NODE_REV_ID |
| pair and adds it to the NODE_ORIGINS_PATH file. */ |
| static svn_error_t * |
| set_node_origins_for_file(svn_fs_t *fs, |
| const char *node_origins_path, |
| const char *node_id, |
| svn_string_t *node_rev_id, |
| apr_pool_t *pool) |
| { |
| const char *path_tmp; |
| svn_stream_t *stream; |
| apr_hash_t *origins_hash; |
| svn_string_t *old_node_rev_id; |
| |
| SVN_ERR(svn_fs_fs__ensure_dir_exists(svn_dirent_join(fs->path, |
| PATH_NODE_ORIGINS_DIR, |
| pool), |
| fs->path, pool)); |
| |
| /* Read the previously existing origins (if any), and merge our |
| update with it. */ |
| SVN_ERR(get_node_origins_from_file(fs, &origins_hash, |
| node_origins_path, pool)); |
| if (! origins_hash) |
| origins_hash = apr_hash_make(pool); |
| |
| old_node_rev_id = svn_hash_gets(origins_hash, node_id); |
| |
| if (old_node_rev_id && !svn_string_compare(node_rev_id, old_node_rev_id)) |
| return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, |
| _("Node origin for '%s' exists with a different " |
| "value (%s) than what we were about to store " |
| "(%s)"), |
| node_id, old_node_rev_id->data, node_rev_id->data); |
| |
| svn_hash_sets(origins_hash, node_id, node_rev_id); |
| |
| /* Sure, there's a race condition here. Two processes could be |
| trying to add different cache elements to the same file at the |
| same time, and the entries added by the first one to write will |
| be lost. But this is just a cache of reconstructible data, so |
| we'll accept this problem in return for not having to deal with |
| locking overhead. */ |
| |
| /* Create a temporary file, write out our hash, and close the file. */ |
| SVN_ERR(svn_stream_open_unique(&stream, &path_tmp, |
| svn_dirent_dirname(node_origins_path, pool), |
| svn_io_file_del_none, pool, pool)); |
| SVN_ERR(svn_hash_write2(origins_hash, stream, SVN_HASH_TERMINATOR, pool)); |
| SVN_ERR(svn_stream_close(stream)); |
| |
| /* Rename the temp file as the real destination */ |
| return svn_io_file_rename(path_tmp, node_origins_path, pool); |
| } |
| |
| |
| svn_error_t * |
| svn_fs_fs__set_node_origin(svn_fs_t *fs, |
| const char *node_id, |
| const svn_fs_id_t *node_rev_id, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| const char *filename = path_node_origin(fs, node_id, pool); |
| |
| err = set_node_origins_for_file(fs, filename, |
| node_id, |
| svn_fs_fs__id_unparse(node_rev_id, pool), |
| pool); |
| if (err && APR_STATUS_IS_EACCES(err->apr_err)) |
| { |
| /* It's just a cache; stop trying if I can't write. */ |
| svn_error_clear(err); |
| err = NULL; |
| } |
| return svn_error_trace(err); |
| } |
| |
| |
| svn_error_t * |
| svn_fs_fs__list_transactions(apr_array_header_t **names_p, |
| svn_fs_t *fs, |
| apr_pool_t *pool) |
| { |
| const char *txn_dir; |
| apr_hash_t *dirents; |
| apr_hash_index_t *hi; |
| apr_array_header_t *names; |
| apr_size_t ext_len = strlen(PATH_EXT_TXN); |
| |
| names = apr_array_make(pool, 1, sizeof(const char *)); |
| |
| /* Get the transactions directory. */ |
| txn_dir = svn_dirent_join(fs->path, PATH_TXNS_DIR, pool); |
| |
| /* Now find a listing of this directory. */ |
| SVN_ERR(svn_io_get_dirents3(&dirents, txn_dir, TRUE, pool, pool)); |
| |
| /* Loop through all the entries and return anything that ends with '.txn'. */ |
| for (hi = apr_hash_first(pool, dirents); hi; hi = apr_hash_next(hi)) |
| { |
| const char *name = svn__apr_hash_index_key(hi); |
| apr_ssize_t klen = svn__apr_hash_index_klen(hi); |
| const char *id; |
| |
| /* The name must end with ".txn" to be considered a transaction. */ |
| if ((apr_size_t) klen <= ext_len |
| || (strcmp(name + klen - ext_len, PATH_EXT_TXN)) != 0) |
| continue; |
| |
| /* Truncate the ".txn" extension and store the ID. */ |
| id = apr_pstrndup(pool, name, strlen(name) - ext_len); |
| APR_ARRAY_PUSH(names, const char *) = id; |
| } |
| |
| *names_p = names; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__open_txn(svn_fs_txn_t **txn_p, |
| svn_fs_t *fs, |
| const char *name, |
| apr_pool_t *pool) |
| { |
| svn_fs_txn_t *txn; |
| svn_node_kind_t kind; |
| transaction_t *local_txn; |
| |
| /* First check to see if the directory exists. */ |
| SVN_ERR(svn_io_check_path(path_txn_dir(fs, name, pool), &kind, pool)); |
| |
| /* Did we find it? */ |
| if (kind != svn_node_dir) |
| return svn_error_createf(SVN_ERR_FS_NO_SUCH_TRANSACTION, NULL, |
| _("No such transaction '%s'"), |
| name); |
| |
| txn = apr_pcalloc(pool, sizeof(*txn)); |
| |
| /* Read in the root node of this transaction. */ |
| txn->id = apr_pstrdup(pool, name); |
| txn->fs = fs; |
| |
| SVN_ERR(svn_fs_fs__get_txn(&local_txn, fs, name, pool)); |
| |
| txn->base_rev = svn_fs_fs__id_rev(local_txn->base_id); |
| |
| txn->vtable = &txn_vtable; |
| *txn_p = txn; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__txn_proplist(apr_hash_t **table_p, |
| svn_fs_txn_t *txn, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *proplist = apr_hash_make(pool); |
| SVN_ERR(get_txn_proplist(proplist, txn->fs, txn->id, pool)); |
| *table_p = proplist; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__delete_node_revision(svn_fs_t *fs, |
| const svn_fs_id_t *id, |
| apr_pool_t *pool) |
| { |
| node_revision_t *noderev; |
| |
| SVN_ERR(svn_fs_fs__get_node_revision(&noderev, fs, id, pool)); |
| |
| /* Delete any mutable property representation. */ |
| if (noderev->prop_rep && noderev->prop_rep->txn_id) |
| SVN_ERR(svn_io_remove_file2(path_txn_node_props(fs, id, pool), FALSE, |
| pool)); |
| |
| /* Delete any mutable data representation. */ |
| if (noderev->data_rep && noderev->data_rep->txn_id |
| && noderev->kind == svn_node_dir) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| SVN_ERR(svn_io_remove_file2(path_txn_node_children(fs, id, pool), FALSE, |
| pool)); |
| |
| /* remove the corresponding entry from the cache, if such exists */ |
| if (ffd->txn_dir_cache) |
| { |
| const char *key = svn_fs_fs__id_unparse(id, pool)->data; |
| SVN_ERR(svn_cache__set(ffd->txn_dir_cache, key, NULL, pool)); |
| } |
| } |
| |
| return svn_io_remove_file2(path_txn_node_rev(fs, id, pool), FALSE, pool); |
| } |
| |
| |
| |
| /*** Revisions ***/ |
| |
| svn_error_t * |
| svn_fs_fs__revision_prop(svn_string_t **value_p, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| const char *propname, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *table; |
| |
| SVN_ERR(svn_fs__check_fs(fs, TRUE)); |
| SVN_ERR(svn_fs_fs__revision_proplist(&table, fs, rev, pool)); |
| |
| *value_p = svn_hash_gets(table, propname); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Baton used for change_rev_prop_body below. */ |
| struct change_rev_prop_baton { |
| svn_fs_t *fs; |
| svn_revnum_t rev; |
| const char *name; |
| const svn_string_t *const *old_value_p; |
| const svn_string_t *value; |
| }; |
| |
| /* The work-horse for svn_fs_fs__change_rev_prop, called with the FS |
| write lock. This implements the svn_fs_fs__with_write_lock() |
| 'body' callback type. BATON is a 'struct change_rev_prop_baton *'. */ |
| static svn_error_t * |
| change_rev_prop_body(void *baton, apr_pool_t *pool) |
| { |
| struct change_rev_prop_baton *cb = baton; |
| apr_hash_t *table; |
| |
| SVN_ERR(svn_fs_fs__revision_proplist(&table, cb->fs, cb->rev, pool)); |
| |
| if (cb->old_value_p) |
| { |
| const svn_string_t *wanted_value = *cb->old_value_p; |
| const svn_string_t *present_value = svn_hash_gets(table, cb->name); |
| if ((!wanted_value != !present_value) |
| || (wanted_value && present_value |
| && !svn_string_compare(wanted_value, present_value))) |
| { |
| /* What we expected isn't what we found. */ |
| return svn_error_createf(SVN_ERR_FS_PROP_BASEVALUE_MISMATCH, NULL, |
| _("revprop '%s' has unexpected value in " |
| "filesystem"), |
| cb->name); |
| } |
| /* Fall through. */ |
| } |
| svn_hash_sets(table, cb->name, cb->value); |
| |
| return set_revision_proplist(cb->fs, cb->rev, table, pool); |
| } |
| |
| svn_error_t * |
| svn_fs_fs__change_rev_prop(svn_fs_t *fs, |
| svn_revnum_t rev, |
| const char *name, |
| const svn_string_t *const *old_value_p, |
| const svn_string_t *value, |
| apr_pool_t *pool) |
| { |
| struct change_rev_prop_baton cb; |
| |
| SVN_ERR(svn_fs__check_fs(fs, TRUE)); |
| |
| cb.fs = fs; |
| cb.rev = rev; |
| cb.name = name; |
| cb.old_value_p = old_value_p; |
| cb.value = value; |
| |
| return svn_fs_fs__with_write_lock(fs, change_rev_prop_body, &cb, pool); |
| } |
| |
| |
| |
| /*** Transactions ***/ |
| |
| svn_error_t * |
| svn_fs_fs__get_txn_ids(const svn_fs_id_t **root_id_p, |
| const svn_fs_id_t **base_root_id_p, |
| svn_fs_t *fs, |
| const char *txn_name, |
| apr_pool_t *pool) |
| { |
| transaction_t *txn; |
| SVN_ERR(svn_fs_fs__get_txn(&txn, fs, txn_name, pool)); |
| *root_id_p = txn->root_id; |
| *base_root_id_p = txn->base_id; |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Generic transaction operations. */ |
| |
| svn_error_t * |
| svn_fs_fs__txn_prop(svn_string_t **value_p, |
| svn_fs_txn_t *txn, |
| const char *propname, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *table; |
| svn_fs_t *fs = txn->fs; |
| |
| SVN_ERR(svn_fs__check_fs(fs, TRUE)); |
| SVN_ERR(svn_fs_fs__txn_proplist(&table, txn, pool)); |
| |
| *value_p = svn_hash_gets(table, propname); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__begin_txn(svn_fs_txn_t **txn_p, |
| svn_fs_t *fs, |
| svn_revnum_t rev, |
| apr_uint32_t flags, |
| apr_pool_t *pool) |
| { |
| svn_string_t date; |
| svn_prop_t prop; |
| apr_array_header_t *props = apr_array_make(pool, 3, sizeof(svn_prop_t)); |
| |
| SVN_ERR(svn_fs__check_fs(fs, TRUE)); |
| |
| SVN_ERR(svn_fs_fs__create_txn(txn_p, fs, rev, pool)); |
| |
| /* Put a datestamp on the newly created txn, so we always know |
| exactly how old it is. (This will help sysadmins identify |
| long-abandoned txns that may need to be manually removed.) When |
| a txn is promoted to a revision, this property will be |
| automatically overwritten with a revision datestamp. */ |
| date.data = svn_time_to_cstring(apr_time_now(), pool); |
| date.len = strlen(date.data); |
| |
| prop.name = SVN_PROP_REVISION_DATE; |
| prop.value = &date; |
| APR_ARRAY_PUSH(props, svn_prop_t) = prop; |
| |
| /* Set temporary txn props that represent the requested 'flags' |
| behaviors. */ |
| if (flags & SVN_FS_TXN_CHECK_OOD) |
| { |
| prop.name = SVN_FS__PROP_TXN_CHECK_OOD; |
| prop.value = svn_string_create("true", pool); |
| APR_ARRAY_PUSH(props, svn_prop_t) = prop; |
| } |
| |
| if (flags & SVN_FS_TXN_CHECK_LOCKS) |
| { |
| prop.name = SVN_FS__PROP_TXN_CHECK_LOCKS; |
| prop.value = svn_string_create("true", pool); |
| APR_ARRAY_PUSH(props, svn_prop_t) = prop; |
| } |
| |
| return svn_fs_fs__change_txn_props(*txn_p, props, pool); |
| } |
| |
| |
| /****** Packing FSFS shards *********/ |
| |
| /* Write a file FILENAME in directory FS_PATH, containing a single line |
| * with the number REVNUM in ASCII decimal. Move the file into place |
| * atomically, overwriting any existing file. |
| * |
| * Similar to write_current(). */ |
| static svn_error_t * |
| write_revnum_file(const char *fs_path, |
| const char *filename, |
| svn_revnum_t revnum, |
| apr_pool_t *scratch_pool) |
| { |
| const char *final_path, *tmp_path; |
| svn_stream_t *tmp_stream; |
| |
| final_path = svn_dirent_join(fs_path, filename, scratch_pool); |
| SVN_ERR(svn_stream_open_unique(&tmp_stream, &tmp_path, fs_path, |
| svn_io_file_del_none, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_stream_printf(tmp_stream, scratch_pool, "%ld\n", revnum)); |
| SVN_ERR(svn_stream_close(tmp_stream)); |
| SVN_ERR(move_into_place(tmp_path, final_path, final_path, scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Pack the revision SHARD containing exactly MAX_FILES_PER_DIR revisions |
| * from SHARD_PATH into the PACK_FILE_DIR, using POOL for allocations. |
| * CANCEL_FUNC and CANCEL_BATON are what you think they are. |
| * |
| * If for some reason we detect a partial packing already performed, we |
| * remove the pack file and start again. |
| */ |
| static svn_error_t * |
| pack_rev_shard(const char *pack_file_dir, |
| const char *shard_path, |
| apr_int64_t shard, |
| int max_files_per_dir, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *pool) |
| { |
| const char *pack_file_path, *manifest_file_path; |
| svn_stream_t *pack_stream, *manifest_stream; |
| svn_revnum_t start_rev, end_rev, rev; |
| apr_off_t next_offset; |
| apr_pool_t *iterpool; |
| |
| /* Some useful paths. */ |
| pack_file_path = svn_dirent_join(pack_file_dir, PATH_PACKED, pool); |
| manifest_file_path = svn_dirent_join(pack_file_dir, PATH_MANIFEST, pool); |
| |
| /* Remove any existing pack file for this shard, since it is incomplete. */ |
| SVN_ERR(svn_io_remove_dir2(pack_file_dir, TRUE, cancel_func, cancel_baton, |
| pool)); |
| |
| /* Create the new directory and pack and manifest files. */ |
| SVN_ERR(svn_io_dir_make(pack_file_dir, APR_OS_DEFAULT, pool)); |
| SVN_ERR(svn_stream_open_writable(&pack_stream, pack_file_path, pool, |
| pool)); |
| SVN_ERR(svn_stream_open_writable(&manifest_stream, manifest_file_path, |
| pool, pool)); |
| |
| start_rev = (svn_revnum_t) (shard * max_files_per_dir); |
| end_rev = (svn_revnum_t) ((shard + 1) * (max_files_per_dir) - 1); |
| next_offset = 0; |
| iterpool = svn_pool_create(pool); |
| |
| /* Iterate over the revisions in this shard, squashing them together. */ |
| for (rev = start_rev; rev <= end_rev; rev++) |
| { |
| svn_stream_t *rev_stream; |
| apr_finfo_t finfo; |
| const char *path; |
| |
| svn_pool_clear(iterpool); |
| |
| /* Get the size of the file. */ |
| path = svn_dirent_join(shard_path, apr_psprintf(iterpool, "%ld", rev), |
| iterpool); |
| SVN_ERR(svn_io_stat(&finfo, path, APR_FINFO_SIZE, iterpool)); |
| |
| /* Update the manifest. */ |
| SVN_ERR(svn_stream_printf(manifest_stream, iterpool, "%" APR_OFF_T_FMT |
| "\n", next_offset)); |
| next_offset += finfo.size; |
| |
| /* Copy all the bits from the rev file to the end of the pack file. */ |
| SVN_ERR(svn_stream_open_readonly(&rev_stream, path, iterpool, iterpool)); |
| SVN_ERR(svn_stream_copy3(rev_stream, svn_stream_disown(pack_stream, |
| iterpool), |
| cancel_func, cancel_baton, iterpool)); |
| } |
| |
| SVN_ERR(svn_stream_close(manifest_stream)); |
| SVN_ERR(svn_stream_close(pack_stream)); |
| SVN_ERR(svn_io_copy_perms(shard_path, pack_file_dir, iterpool)); |
| SVN_ERR(svn_io_set_file_read_only(pack_file_path, FALSE, iterpool)); |
| SVN_ERR(svn_io_set_file_read_only(manifest_file_path, FALSE, iterpool)); |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Copy revprop files for revisions [START_REV, END_REV) from SHARD_PATH |
| * to the pack file at PACK_FILE_NAME in PACK_FILE_DIR. |
| * |
| * The file sizes have already been determined and written to SIZES. |
| * Please note that this function will be executed while the filesystem |
| * has been locked and that revprops files will therefore not be modified |
| * while the pack is in progress. |
| * |
| * COMPRESSION_LEVEL defines how well the resulting pack file shall be |
| * compressed or whether is shall be compressed at all. TOTAL_SIZE is |
| * a hint on which initial buffer size we should use to hold the pack file |
| * content. |
| * |
| * CANCEL_FUNC and CANCEL_BATON are used as usual. Temporary allocations |
| * are done in SCRATCH_POOL. |
| */ |
| static svn_error_t * |
| copy_revprops(const char *pack_file_dir, |
| const char *pack_filename, |
| const char *shard_path, |
| svn_revnum_t start_rev, |
| svn_revnum_t end_rev, |
| apr_array_header_t *sizes, |
| apr_size_t total_size, |
| int compression_level, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *scratch_pool) |
| { |
| svn_stream_t *pack_stream; |
| apr_file_t *pack_file; |
| svn_revnum_t rev; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| svn_stream_t *stream; |
| |
| /* create empty data buffer and a write stream on top of it */ |
| svn_stringbuf_t *uncompressed |
| = svn_stringbuf_create_ensure(total_size, scratch_pool); |
| svn_stringbuf_t *compressed |
| = svn_stringbuf_create_empty(scratch_pool); |
| pack_stream = svn_stream_from_stringbuf(uncompressed, scratch_pool); |
| |
| /* write the pack file header */ |
| SVN_ERR(serialize_revprops_header(pack_stream, start_rev, sizes, 0, |
| sizes->nelts, iterpool)); |
| |
| /* Some useful paths. */ |
| SVN_ERR(svn_io_file_open(&pack_file, svn_dirent_join(pack_file_dir, |
| pack_filename, |
| scratch_pool), |
| APR_WRITE | APR_CREATE, APR_OS_DEFAULT, |
| scratch_pool)); |
| |
| /* Iterate over the revisions in this shard, squashing them together. */ |
| for (rev = start_rev; rev <= end_rev; rev++) |
| { |
| const char *path; |
| |
| svn_pool_clear(iterpool); |
| |
| /* Construct the file name. */ |
| path = svn_dirent_join(shard_path, apr_psprintf(iterpool, "%ld", rev), |
| iterpool); |
| |
| /* Copy all the bits from the non-packed revprop file to the end of |
| * the pack file. */ |
| SVN_ERR(svn_stream_open_readonly(&stream, path, iterpool, iterpool)); |
| SVN_ERR(svn_stream_copy3(stream, pack_stream, |
| cancel_func, cancel_baton, iterpool)); |
| } |
| |
| /* flush stream buffers to content buffer */ |
| SVN_ERR(svn_stream_close(pack_stream)); |
| |
| /* compress the content (or just store it for COMPRESSION_LEVEL 0) */ |
| SVN_ERR(svn__compress(svn_stringbuf__morph_into_string(uncompressed), |
| compressed, compression_level)); |
| |
| /* write the pack file content to disk */ |
| stream = svn_stream_from_aprfile2(pack_file, FALSE, scratch_pool); |
| SVN_ERR(svn_stream_write(stream, compressed->data, &compressed->len)); |
| SVN_ERR(svn_stream_close(stream)); |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* For the revprop SHARD at SHARD_PATH with exactly MAX_FILES_PER_DIR |
| * revprop files in it, create a packed shared at PACK_FILE_DIR. |
| * |
| * COMPRESSION_LEVEL defines how well the resulting pack file shall be |
| * compressed or whether is shall be compressed at all. Individual pack |
| * file containing more than one revision will be limited to a size of |
| * MAX_PACK_SIZE bytes before compression. |
| * |
| * CANCEL_FUNC and CANCEL_BATON are used in the usual way. Temporary |
| * allocations are done in SCRATCH_POOL. |
| */ |
| static svn_error_t * |
| pack_revprops_shard(const char *pack_file_dir, |
| const char *shard_path, |
| apr_int64_t shard, |
| int max_files_per_dir, |
| apr_off_t max_pack_size, |
| int compression_level, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *scratch_pool) |
| { |
| const char *manifest_file_path, *pack_filename = NULL; |
| svn_stream_t *manifest_stream; |
| svn_revnum_t start_rev, end_rev, rev; |
| apr_off_t total_size; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| apr_array_header_t *sizes; |
| |
| /* Some useful paths. */ |
| manifest_file_path = svn_dirent_join(pack_file_dir, PATH_MANIFEST, |
| scratch_pool); |
| |
| /* Remove any existing pack file for this shard, since it is incomplete. */ |
| SVN_ERR(svn_io_remove_dir2(pack_file_dir, TRUE, cancel_func, cancel_baton, |
| scratch_pool)); |
| |
| /* Create the new directory and manifest file stream. */ |
| SVN_ERR(svn_io_dir_make(pack_file_dir, APR_OS_DEFAULT, scratch_pool)); |
| SVN_ERR(svn_stream_open_writable(&manifest_stream, manifest_file_path, |
| scratch_pool, scratch_pool)); |
| |
| /* revisions to handle. Special case: revision 0 */ |
| start_rev = (svn_revnum_t) (shard * max_files_per_dir); |
| end_rev = (svn_revnum_t) ((shard + 1) * (max_files_per_dir) - 1); |
| if (start_rev == 0) |
| ++start_rev; |
| |
| /* initialize the revprop size info */ |
| sizes = apr_array_make(scratch_pool, max_files_per_dir, sizeof(apr_off_t)); |
| total_size = 2 * SVN_INT64_BUFFER_SIZE; |
| |
| /* Iterate over the revisions in this shard, determine their size and |
| * squashing them together into pack files. */ |
| for (rev = start_rev; rev <= end_rev; rev++) |
| { |
| apr_finfo_t finfo; |
| const char *path; |
| |
| svn_pool_clear(iterpool); |
| |
| /* Get the size of the file. */ |
| path = svn_dirent_join(shard_path, apr_psprintf(iterpool, "%ld", rev), |
| iterpool); |
| SVN_ERR(svn_io_stat(&finfo, path, APR_FINFO_SIZE, iterpool)); |
| |
| /* if we already have started a pack file and this revprop cannot be |
| * appended to it, write the previous pack file. */ |
| if (sizes->nelts != 0 && |
| total_size + SVN_INT64_BUFFER_SIZE + finfo.size > max_pack_size) |
| { |
| SVN_ERR(copy_revprops(pack_file_dir, pack_filename, shard_path, |
| start_rev, rev-1, sizes, (apr_size_t)total_size, |
| compression_level, cancel_func, cancel_baton, |
| iterpool)); |
| |
| /* next pack file starts empty again */ |
| apr_array_clear(sizes); |
| total_size = 2 * SVN_INT64_BUFFER_SIZE; |
| start_rev = rev; |
| } |
| |
| /* Update the manifest. Allocate a file name for the current pack |
| * file if it is a new one */ |
| if (sizes->nelts == 0) |
| pack_filename = apr_psprintf(scratch_pool, "%ld.0", rev); |
| |
| SVN_ERR(svn_stream_printf(manifest_stream, iterpool, "%s\n", |
| pack_filename)); |
| |
| /* add to list of files to put into the current pack file */ |
| APR_ARRAY_PUSH(sizes, apr_off_t) = finfo.size; |
| total_size += SVN_INT64_BUFFER_SIZE + finfo.size; |
| } |
| |
| /* write the last pack file */ |
| if (sizes->nelts != 0) |
| SVN_ERR(copy_revprops(pack_file_dir, pack_filename, shard_path, |
| start_rev, rev-1, sizes, (apr_size_t)total_size, |
| compression_level, cancel_func, cancel_baton, |
| iterpool)); |
| |
| /* flush the manifest file and update permissions */ |
| SVN_ERR(svn_stream_close(manifest_stream)); |
| SVN_ERR(svn_io_copy_perms(shard_path, pack_file_dir, iterpool)); |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Delete the non-packed revprop SHARD at SHARD_PATH with exactly |
| * MAX_FILES_PER_DIR revprop files in it. If this is shard 0, keep the |
| * revprop file for revision 0. |
| * |
| * CANCEL_FUNC and CANCEL_BATON are used in the usual way. Temporary |
| * allocations are done in SCRATCH_POOL. |
| */ |
| static svn_error_t * |
| delete_revprops_shard(const char *shard_path, |
| apr_int64_t shard, |
| int max_files_per_dir, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *scratch_pool) |
| { |
| if (shard == 0) |
| { |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| int i; |
| |
| /* delete all files except the one for revision 0 */ |
| for (i = 1; i < max_files_per_dir; ++i) |
| { |
| const char *path = svn_dirent_join(shard_path, |
| apr_psprintf(iterpool, "%d", i), |
| iterpool); |
| if (cancel_func) |
| SVN_ERR((*cancel_func)(cancel_baton)); |
| |
| SVN_ERR(svn_io_remove_file2(path, TRUE, iterpool)); |
| svn_pool_clear(iterpool); |
| } |
| |
| svn_pool_destroy(iterpool); |
| } |
| else |
| SVN_ERR(svn_io_remove_dir2(shard_path, TRUE, |
| cancel_func, cancel_baton, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* In the file system at FS_PATH, pack the SHARD in REVS_DIR and |
| * REVPROPS_DIR containing exactly MAX_FILES_PER_DIR revisions, using POOL |
| * for allocations. REVPROPS_DIR will be NULL if revprop packing is not |
| * supported. COMPRESSION_LEVEL and MAX_PACK_SIZE will be ignored in that |
| * case. |
| * |
| * CANCEL_FUNC and CANCEL_BATON are what you think they are; similarly |
| * NOTIFY_FUNC and NOTIFY_BATON. |
| * |
| * If for some reason we detect a partial packing already performed, we |
| * remove the pack file and start again. |
| */ |
| static svn_error_t * |
| pack_shard(const char *revs_dir, |
| const char *revsprops_dir, |
| const char *fs_path, |
| apr_int64_t shard, |
| int max_files_per_dir, |
| apr_off_t max_pack_size, |
| int compression_level, |
| svn_fs_pack_notify_t notify_func, |
| void *notify_baton, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *pool) |
| { |
| const char *rev_shard_path, *rev_pack_file_dir; |
| const char *revprops_shard_path, *revprops_pack_file_dir; |
| |
| /* Notify caller we're starting to pack this shard. */ |
| if (notify_func) |
| SVN_ERR(notify_func(notify_baton, shard, svn_fs_pack_notify_start, |
| pool)); |
| |
| /* Some useful paths. */ |
| rev_pack_file_dir = svn_dirent_join(revs_dir, |
| apr_psprintf(pool, |
| "%" APR_INT64_T_FMT PATH_EXT_PACKED_SHARD, |
| shard), |
| pool); |
| rev_shard_path = svn_dirent_join(revs_dir, |
| apr_psprintf(pool, "%" APR_INT64_T_FMT, shard), |
| pool); |
| |
| /* pack the revision content */ |
| SVN_ERR(pack_rev_shard(rev_pack_file_dir, rev_shard_path, |
| shard, max_files_per_dir, |
| cancel_func, cancel_baton, pool)); |
| |
| /* if enabled, pack the revprops in an equivalent way */ |
| if (revsprops_dir) |
| { |
| revprops_pack_file_dir = svn_dirent_join(revsprops_dir, |
| apr_psprintf(pool, |
| "%" APR_INT64_T_FMT PATH_EXT_PACKED_SHARD, |
| shard), |
| pool); |
| revprops_shard_path = svn_dirent_join(revsprops_dir, |
| apr_psprintf(pool, "%" APR_INT64_T_FMT, shard), |
| pool); |
| |
| SVN_ERR(pack_revprops_shard(revprops_pack_file_dir, revprops_shard_path, |
| shard, max_files_per_dir, |
| (int)(0.9 * max_pack_size), |
| compression_level, |
| cancel_func, cancel_baton, pool)); |
| } |
| |
| /* Update the min-unpacked-rev file to reflect our newly packed shard. |
| * (This doesn't update ffd->min_unpacked_rev. That will be updated by |
| * update_min_unpacked_rev() when necessary.) */ |
| SVN_ERR(write_revnum_file(fs_path, PATH_MIN_UNPACKED_REV, |
| (svn_revnum_t)((shard + 1) * max_files_per_dir), |
| pool)); |
| |
| /* Finally, remove the existing shard directories. */ |
| SVN_ERR(svn_io_remove_dir2(rev_shard_path, TRUE, |
| cancel_func, cancel_baton, pool)); |
| if (revsprops_dir) |
| SVN_ERR(delete_revprops_shard(revprops_shard_path, |
| shard, max_files_per_dir, |
| cancel_func, cancel_baton, pool)); |
| |
| /* Notify caller we're starting to pack this shard. */ |
| if (notify_func) |
| SVN_ERR(notify_func(notify_baton, shard, svn_fs_pack_notify_end, |
| pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| struct pack_baton |
| { |
| svn_fs_t *fs; |
| svn_fs_pack_notify_t notify_func; |
| void *notify_baton; |
| svn_cancel_func_t cancel_func; |
| void *cancel_baton; |
| }; |
| |
| |
| /* The work-horse for svn_fs_fs__pack, called with the FS write lock. |
| This implements the svn_fs_fs__with_write_lock() 'body' callback |
| type. BATON is a 'struct pack_baton *'. |
| |
| WARNING: if you add a call to this function, please note: |
| The code currently assumes that any piece of code running with |
| the write-lock set can rely on the ffd->min_unpacked_rev and |
| ffd->min_unpacked_revprop caches to be up-to-date (and, by |
| extension, on not having to use a retry when calling |
| svn_fs_fs__path_rev_absolute() and friends). If you add a call |
| to this function, consider whether you have to call |
| update_min_unpacked_rev(). |
| See this thread: http://thread.gmane.org/1291206765.3782.3309.camel@edith |
| */ |
| static svn_error_t * |
| pack_body(void *baton, |
| apr_pool_t *pool) |
| { |
| struct pack_baton *pb = baton; |
| fs_fs_data_t ffd = {0}; |
| apr_int64_t completed_shards; |
| apr_int64_t i; |
| svn_revnum_t youngest; |
| apr_pool_t *iterpool; |
| const char *rev_data_path; |
| const char *revprops_data_path = NULL; |
| |
| /* read repository settings */ |
| SVN_ERR(read_format(&ffd.format, &ffd.max_files_per_dir, |
| path_format(pb->fs, pool), pool)); |
| SVN_ERR(check_format(ffd.format)); |
| SVN_ERR(read_config(&ffd, pb->fs->path, pool)); |
| |
| /* If the repository isn't a new enough format, we don't support packing. |
| Return a friendly error to that effect. */ |
| if (ffd.format < SVN_FS_FS__MIN_PACKED_FORMAT) |
| return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| _("FSFS format (%d) too old to pack; please upgrade the filesystem."), |
| ffd.format); |
| |
| /* If we aren't using sharding, we can't do any packing, so quit. */ |
| if (!ffd.max_files_per_dir) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(read_min_unpacked_rev(&ffd.min_unpacked_rev, |
| path_min_unpacked_rev(pb->fs, pool), |
| pool)); |
| |
| SVN_ERR(get_youngest(&youngest, pb->fs->path, pool)); |
| completed_shards = (youngest + 1) / ffd.max_files_per_dir; |
| |
| /* See if we've already completed all possible shards thus far. */ |
| if (ffd.min_unpacked_rev == (completed_shards * ffd.max_files_per_dir)) |
| return SVN_NO_ERROR; |
| |
| rev_data_path = svn_dirent_join(pb->fs->path, PATH_REVS_DIR, pool); |
| if (ffd.format >= SVN_FS_FS__MIN_PACKED_REVPROP_FORMAT) |
| revprops_data_path = svn_dirent_join(pb->fs->path, PATH_REVPROPS_DIR, |
| pool); |
| |
| iterpool = svn_pool_create(pool); |
| for (i = ffd.min_unpacked_rev / ffd.max_files_per_dir; |
| i < completed_shards; |
| i++) |
| { |
| svn_pool_clear(iterpool); |
| |
| if (pb->cancel_func) |
| SVN_ERR(pb->cancel_func(pb->cancel_baton)); |
| |
| SVN_ERR(pack_shard(rev_data_path, revprops_data_path, |
| pb->fs->path, i, ffd.max_files_per_dir, |
| ffd.revprop_pack_size, |
| ffd.compress_packed_revprops |
| ? SVN_DELTA_COMPRESSION_LEVEL_DEFAULT |
| : SVN_DELTA_COMPRESSION_LEVEL_NONE, |
| pb->notify_func, pb->notify_baton, |
| pb->cancel_func, pb->cancel_baton, iterpool)); |
| } |
| |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__pack(svn_fs_t *fs, |
| svn_fs_pack_notify_t notify_func, |
| void *notify_baton, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *pool) |
| { |
| struct pack_baton pb = { 0 }; |
| pb.fs = fs; |
| pb.notify_func = notify_func; |
| pb.notify_baton = notify_baton; |
| pb.cancel_func = cancel_func; |
| pb.cancel_baton = cancel_baton; |
| return svn_fs_fs__with_write_lock(fs, pack_body, &pb, pool); |
| } |
| |
| |
| /** Verifying. **/ |
| |
| /* Baton type expected by verify_walker(). The purpose is to reuse open |
| * rev / pack file handles between calls. Its contents need to be cleaned |
| * periodically to limit resource usage. |
| */ |
| typedef struct verify_walker_baton_t |
| { |
| /* number of calls to verify_walker() since the last clean */ |
| int iteration_count; |
| |
| /* number of files opened since the last clean */ |
| int file_count; |
| |
| /* progress notification callback to invoke periodically (may be NULL) */ |
| svn_fs_progress_notify_func_t notify_func; |
| |
| /* baton to use with NOTIFY_FUNC */ |
| void *notify_baton; |
| |
| /* remember the last revision for which we called notify_func */ |
| svn_revnum_t last_notified_revision; |
| |
| /* current file handle (or NULL) */ |
| apr_file_t *file_hint; |
| |
| /* corresponding revision (or SVN_INVALID_REVNUM) */ |
| svn_revnum_t rev_hint; |
| |
| /* pool to use for the file handles etc. */ |
| apr_pool_t *pool; |
| } verify_walker_baton_t; |
| |
| /* Used by svn_fs_fs__verify(). |
| Implements svn_fs_fs__walk_rep_reference().walker. */ |
| static svn_error_t * |
| verify_walker(representation_t *rep, |
| void *baton, |
| svn_fs_t *fs, |
| apr_pool_t *scratch_pool) |
| { |
| struct rep_state *rs; |
| struct rep_args *rep_args; |
| |
| if (baton) |
| { |
| verify_walker_baton_t *walker_baton = baton; |
| apr_file_t * previous_file; |
| |
| /* notify and free resources periodically */ |
| if ( walker_baton->iteration_count > 1000 |
| || walker_baton->file_count > 16) |
| { |
| if ( walker_baton->notify_func |
| && rep->revision != walker_baton->last_notified_revision) |
| { |
| walker_baton->notify_func(rep->revision, |
| walker_baton->notify_baton, |
| scratch_pool); |
| walker_baton->last_notified_revision = rep->revision; |
| } |
| |
| svn_pool_clear(walker_baton->pool); |
| |
| walker_baton->iteration_count = 0; |
| walker_baton->file_count = 0; |
| walker_baton->file_hint = NULL; |
| walker_baton->rev_hint = SVN_INVALID_REVNUM; |
| } |
| |
| /* access the repo data */ |
| previous_file = walker_baton->file_hint; |
| SVN_ERR(create_rep_state(&rs, &rep_args, &walker_baton->file_hint, |
| &walker_baton->rev_hint, rep, fs, |
| walker_baton->pool)); |
| |
| /* update resource usage counters */ |
| walker_baton->iteration_count++; |
| if (previous_file != walker_baton->file_hint) |
| walker_baton->file_count++; |
| } |
| else |
| { |
| /* ### Should this be using read_rep_line() directly? */ |
| SVN_ERR(create_rep_state(&rs, &rep_args, NULL, NULL, rep, fs, |
| scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_fs_fs__verify(svn_fs_t *fs, |
| svn_revnum_t start, |
| svn_revnum_t end, |
| svn_fs_progress_notify_func_t notify_func, |
| void *notify_baton, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *ffd = fs->fsap_data; |
| svn_boolean_t exists; |
| svn_revnum_t youngest = ffd->youngest_rev_cache; /* cache is current */ |
| |
| if (ffd->format < SVN_FS_FS__MIN_REP_SHARING_FORMAT) |
| return SVN_NO_ERROR; |
| |
| /* Input validation. */ |
| if (! SVN_IS_VALID_REVNUM(start)) |
| start = 0; |
| if (! SVN_IS_VALID_REVNUM(end)) |
| end = youngest; |
| SVN_ERR(ensure_revision_exists(fs, start, pool)); |
| SVN_ERR(ensure_revision_exists(fs, end, pool)); |
| |
| /* rep-cache verification. */ |
| SVN_ERR(svn_fs_fs__exists_rep_cache(&exists, fs, pool)); |
| if (exists) |
| { |
| /* provide a baton to allow the reuse of open file handles between |
| iterations (saves 2/3 of OS level file operations). */ |
| verify_walker_baton_t *baton = apr_pcalloc(pool, sizeof(*baton)); |
| baton->rev_hint = SVN_INVALID_REVNUM; |
| baton->pool = svn_pool_create(pool); |
| baton->last_notified_revision = SVN_INVALID_REVNUM; |
| baton->notify_func = notify_func; |
| baton->notify_baton = notify_baton; |
| |
| /* tell the user that we are now ready to do *something* */ |
| if (notify_func) |
| notify_func(SVN_INVALID_REVNUM, notify_baton, baton->pool); |
| |
| /* Do not attempt to walk the rep-cache database if its file does |
| not exist, since doing so would create it --- which may confuse |
| the administrator. Don't take any lock. */ |
| SVN_ERR(svn_fs_fs__walk_rep_reference(fs, start, end, |
| verify_walker, baton, |
| cancel_func, cancel_baton, |
| pool)); |
| |
| /* walker resource cleanup */ |
| svn_pool_destroy(baton->pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /** Hotcopy. **/ |
| |
| /* 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. */ |
| static svn_error_t * |
| hotcopy_io_dir_file_copy(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; |
| } |
| |
| 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. */ |
| static svn_error_t * |
| hotcopy_io_copy_dir_recursively(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(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(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. |
| * Use SCRATCH_POOL for temporary allocations. */ |
| static svn_error_t * |
| hotcopy_copy_shard_file(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(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. |
| * Use SCRATCH_POOL for temporary allocations. */ |
| static svn_error_t * |
| hotcopy_copy_packed_shard(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(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(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(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(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(write_revnum_file(dst_fs->path, PATH_MIN_UNPACKED_REV, |
| *dst_min_unpacked_rev, |
| scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* If NEW_YOUNGEST is younger than *DST_YOUNGEST, update the 'current' |
| * file in DST_FS and set *DST_YOUNGEST to NEW_YOUNGEST. |
| * Use SCRATCH_POOL for temporary allocations. */ |
| static svn_error_t * |
| hotcopy_update_current(svn_revnum_t *dst_youngest, |
| svn_fs_t *dst_fs, |
| svn_revnum_t new_youngest, |
| apr_pool_t *scratch_pool) |
| { |
| char next_node_id[MAX_KEY_SIZE] = "0"; |
| char next_copy_id[MAX_KEY_SIZE] = "0"; |
| fs_fs_data_t *dst_ffd = dst_fs->fsap_data; |
| |
| if (*dst_youngest >= new_youngest) |
| return SVN_NO_ERROR; |
| |
| /* If necessary, get new current next_node and next_copy IDs. */ |
| if (dst_ffd->format < SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT) |
| { |
| apr_off_t root_offset; |
| apr_file_t *rev_file; |
| char max_node_id[MAX_KEY_SIZE] = "0"; |
| char max_copy_id[MAX_KEY_SIZE] = "0"; |
| apr_size_t len; |
| |
| if (dst_ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) |
| SVN_ERR(update_min_unpacked_rev(dst_fs, scratch_pool)); |
| |
| SVN_ERR(open_pack_or_rev_file(&rev_file, dst_fs, new_youngest, |
| scratch_pool)); |
| SVN_ERR(get_root_changes_offset(&root_offset, NULL, rev_file, |
| dst_fs, new_youngest, scratch_pool)); |
| SVN_ERR(recover_find_max_ids(dst_fs, new_youngest, rev_file, |
| root_offset, max_node_id, max_copy_id, |
| scratch_pool)); |
| SVN_ERR(svn_io_file_close(rev_file, scratch_pool)); |
| |
| /* We store the _next_ ids. */ |
| len = strlen(max_node_id); |
| svn_fs_fs__next_key(max_node_id, &len, next_node_id); |
| len = strlen(max_copy_id); |
| svn_fs_fs__next_key(max_copy_id, &len, next_copy_id); |
| } |
| |
| /* Update 'current'. */ |
| SVN_ERR(write_current(dst_fs, new_youngest, next_node_id, next_copy_id, |
| scratch_pool)); |
| |
| *dst_youngest = new_youngest; |
| |
| 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++) |
| { |
| const char *path; |
| 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 */ |
| path = svn_dirent_join(dst_subdir_shard, |
| apr_psprintf(iterpool, "%ld", rev), |
| iterpool); |
| |
| /* Make the rev file writable and remove it. */ |
| SVN_ERR(svn_io_set_file_read_write(path, TRUE, iterpool)); |
| SVN_ERR(svn_io_remove_file2(path, TRUE, 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); |
| } |
| |
| /* Baton for hotcopy_body(). */ |
| struct hotcopy_body_baton { |
| svn_fs_t *src_fs; |
| svn_fs_t *dst_fs; |
| svn_boolean_t incremental; |
| svn_cancel_func_t cancel_func; |
| void *cancel_baton; |
| } hotcopy_body_baton; |
| |
| /* Perform a hotcopy, either normal or incremental. |
| * |
| * Normal hotcopy assumes that the destination exists as an empty |
| * directory. It behaves like an incremental hotcopy except that |
| * none of the copied files already exist in the destination. |
| * |
| * An incremental hotcopy copies only changed or new files to the destination, |
| * and removes files from the destination no longer present in the source. |
| * While the incremental hotcopy is running, readers should still be able |
| * to access the destintation repository without error and should not see |
| * revisions currently in progress of being copied. Readers are able to see |
| * new fully copied revisions even if the entire incremental hotcopy procedure |
| * has not yet completed. |
| * |
| * Writers are blocked out completely during the entire incremental hotcopy |
| * process to ensure consistency. This function assumes that the repository |
| * write-lock is held. |
| */ |
| static svn_error_t * |
| hotcopy_body(void *baton, apr_pool_t *pool) |
| { |
| struct hotcopy_body_baton *hbb = baton; |
| svn_fs_t *src_fs = hbb->src_fs; |
| fs_fs_data_t *src_ffd = src_fs->fsap_data; |
| svn_fs_t *dst_fs = hbb->dst_fs; |
| fs_fs_data_t *dst_ffd = dst_fs->fsap_data; |
| int max_files_per_dir = src_ffd->max_files_per_dir; |
| svn_boolean_t incremental = hbb->incremental; |
| svn_cancel_func_t cancel_func = hbb->cancel_func; |
| void* cancel_baton = hbb->cancel_baton; |
| svn_revnum_t src_youngest; |
| svn_revnum_t dst_youngest; |
| svn_revnum_t rev; |
| svn_revnum_t src_min_unpacked_rev; |
| svn_revnum_t dst_min_unpacked_rev; |
| const char *src_subdir; |
| const char *dst_subdir; |
| const char *revprop_src_subdir; |
| const char *revprop_dst_subdir; |
| apr_pool_t *iterpool; |
| 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 *msg; |
| 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); |
| |
| msg = apr_psprintf(pool, |
| _("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); |
| return svn_error_quick_wrap(err, msg); |
| } |
| 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(get_youngest(&src_youngest, src_fs->path, pool)); |
| if (incremental) |
| { |
| SVN_ERR(get_youngest(&dst_youngest, dst_fs->path, 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; |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| /* Copy the min unpacked rev, and read its value. */ |
| if (src_ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) |
| { |
| const char *min_unpacked_rev_path; |
| |
| min_unpacked_rev_path = svn_dirent_join(src_fs->path, |
| PATH_MIN_UNPACKED_REV, |
| pool); |
| SVN_ERR(read_min_unpacked_rev(&src_min_unpacked_rev, |
| min_unpacked_rev_path, |
| pool)); |
| |
| min_unpacked_rev_path = svn_dirent_join(dst_fs->path, |
| PATH_MIN_UNPACKED_REV, |
| pool); |
| SVN_ERR(read_min_unpacked_rev(&dst_min_unpacked_rev, |
| min_unpacked_rev_path, |
| 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. |
| */ |
| |
| src_subdir = svn_dirent_join(src_fs->path, PATH_REVS_DIR, pool); |
| dst_subdir = svn_dirent_join(dst_fs->path, PATH_REVS_DIR, pool); |
| SVN_ERR(svn_io_make_dir_recursively(dst_subdir, pool)); |
| |
| iterpool = svn_pool_create(pool); |
| /* First, copy packed shards. */ |
| for (rev = 0; rev < src_min_unpacked_rev; rev += max_files_per_dir) |
| { |
| svn_pool_clear(iterpool); |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| /* Copy the packed shard. */ |
| SVN_ERR(hotcopy_copy_packed_shard(&dst_min_unpacked_rev, |
| src_fs, dst_fs, |
| rev, max_files_per_dir, |
| iterpool)); |
| |
| /* If necessary, update 'current' to the most recent packed rev, |
| * so readers can see new revisions which arrived in this pack. */ |
| SVN_ERR(hotcopy_update_current(&dst_youngest, dst_fs, |
| rev + max_files_per_dir - 1, |
| 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(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(path_revprops_shard(dst_fs, rev, iterpool), |
| cancel_func, cancel_baton, iterpool)); |
| } |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| /* Now, copy pairs of non-packed revisions and revprop files. |
| * If necessary, update 'current' after copying all files from a shard. */ |
| SVN_ERR_ASSERT(rev == src_min_unpacked_rev); |
| SVN_ERR_ASSERT(src_min_unpacked_rev == dst_min_unpacked_rev); |
| revprop_src_subdir = svn_dirent_join(src_fs->path, PATH_REVPROPS_DIR, pool); |
| revprop_dst_subdir = svn_dirent_join(dst_fs->path, PATH_REVPROPS_DIR, pool); |
| SVN_ERR(svn_io_make_dir_recursively(revprop_dst_subdir, pool)); |
| for (; rev <= src_youngest; rev++) |
| { |
| svn_error_t *err; |
| |
| svn_pool_clear(iterpool); |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| /* Copy the rev file. */ |
| err = hotcopy_copy_shard_file(src_subdir, dst_subdir, |
| rev, max_files_per_dir, |
| iterpool); |
| if (err) |
| { |
| if (APR_STATUS_IS_ENOENT(err->apr_err) && |
| src_ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) |
| { |
| svn_error_clear(err); |
| |
| /* The source rev file does not exist. This can happen if the |
| * source repository is being packed concurrently with this |
| * hotcopy operation. |
| * |
| * If the new revision is now packed, and the youngest revision |
| * we're interested in is not inside this pack, try to copy the |
| * pack instead. |
| * |
| * If the youngest revision ended up being packed, don't try |
| * to be smart and work around this. Just abort the hotcopy. */ |
| SVN_ERR(update_min_unpacked_rev(src_fs, pool)); |
| if (is_packed_rev(src_fs, rev)) |
| { |
| if (is_packed_rev(src_fs, src_youngest)) |
| return svn_error_createf( |
| SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| _("The assumed HEAD revision (%lu) of the " |
| "hotcopy source has been packed while the " |
| "hotcopy was in progress; please restart " |
| "the hotcopy operation"), |
| src_youngest); |
| |
| SVN_ERR(hotcopy_copy_packed_shard(&dst_min_unpacked_rev, |
| src_fs, dst_fs, |
| rev, max_files_per_dir, |
| iterpool)); |
| rev = dst_min_unpacked_rev; |
| continue; |
| } |
| else |
| return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| _("Revision %lu disappeared from the " |
| "hotcopy source while hotcopy was " |
| "in progress"), rev); |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| /* Copy the revprop file. */ |
| SVN_ERR(hotcopy_copy_shard_file(revprop_src_subdir, |
| revprop_dst_subdir, |
| rev, max_files_per_dir, |
| iterpool)); |
| |
| /* After completing a full shard, update 'current'. */ |
| if (max_files_per_dir && rev % max_files_per_dir == 0) |
| SVN_ERR(hotcopy_update_current(&dst_youngest, dst_fs, rev, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| /* 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); |
| |
| /* All revisions were copied. Update 'current'. */ |
| SVN_ERR(hotcopy_update_current(&dst_youngest, dst_fs, src_youngest, 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(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 |
| * younger than the destination's youngest revision. */ |
| 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)); |
| SVN_ERR(svn_fs_fs__del_rep_reference(dst_fs, dst_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)); |
| |
| /* If a revprop generation file exists in the source filesystem, |
| * reset it to zero (since this is on a different path, it will not |
| * overlap with data already in cache). Also, clean up stale files |
| * used for the named atomics implementation. */ |
| SVN_ERR(svn_io_check_path(path_revprop_generation(src_fs, pool), |
| &kind, pool)); |
| if (kind == svn_node_file) |
| SVN_ERR(write_revprop_generation_file(dst_fs, 0, pool)); |
| |
| SVN_ERR(cleanup_revprop_namespace(dst_fs)); |
| |
| /* Hotcopied FS is complete. Stamp it with a format file. */ |
| SVN_ERR(write_format(svn_dirent_join(dst_fs->path, PATH_FORMAT, pool), |
| dst_ffd->format, max_files_per_dir, TRUE, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Set up shared data between SRC_FS and DST_FS. */ |
| static void |
| hotcopy_setup_shared_fs_data(svn_fs_t *src_fs, svn_fs_t *dst_fs) |
| { |
| fs_fs_data_t *src_ffd = src_fs->fsap_data; |
| fs_fs_data_t *dst_ffd = dst_fs->fsap_data; |
| |
| /* The common pool and mutexes are shared between src and dst filesystems. |
| * During hotcopy we only grab the mutexes for the destination, so there |
| * is no risk of dead-lock. We don't write to the src filesystem. Shared |
| * data for the src_fs has already been initialised in fs_hotcopy(). */ |
| dst_ffd->shared = src_ffd->shared; |
| } |
| |
| /* Create an empty filesystem at DST_FS at DST_PATH with the same |
| * configuration as SRC_FS (uuid, format, and other parameters). |
| * After creation DST_FS has no revisions, not even revision zero. */ |
| static svn_error_t * |
| hotcopy_create_empty_dest(svn_fs_t *src_fs, |
| svn_fs_t *dst_fs, |
| const char *dst_path, |
| apr_pool_t *pool) |
| { |
| fs_fs_data_t *src_ffd = src_fs->fsap_data; |
| fs_fs_data_t *dst_ffd = dst_fs->fsap_data; |
| |
| dst_fs->path = apr_pstrdup(pool, dst_path); |
| |
| dst_ffd->max_files_per_dir = src_ffd->max_files_per_dir; |
| dst_ffd->config = src_ffd->config; |
| dst_ffd->format = src_ffd->format; |
| |
| /* Create the revision data directories. */ |
| if (dst_ffd->max_files_per_dir) |
| SVN_ERR(svn_io_make_dir_recursively(path_rev_shard(dst_fs, 0, pool), |
| pool)); |
| else |
| SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(dst_path, |
| PATH_REVS_DIR, pool), |
| pool)); |
| |
| /* Create the revprops directory. */ |
| if (src_ffd->max_files_per_dir) |
| SVN_ERR(svn_io_make_dir_recursively(path_revprops_shard(dst_fs, 0, pool), |
| pool)); |
| else |
| SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(dst_path, |
| PATH_REVPROPS_DIR, |
| pool), |
| pool)); |
| |
| /* Create the transaction directory. */ |
| SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(dst_path, PATH_TXNS_DIR, |
| pool), |
| pool)); |
| |
| /* Create the protorevs directory. */ |
| if (dst_ffd->format >= SVN_FS_FS__MIN_PROTOREVS_DIR_FORMAT) |
| SVN_ERR(svn_io_make_dir_recursively(svn_dirent_join(dst_path, |
| PATH_TXN_PROTOS_DIR, |
| pool), |
| pool)); |
| |
| /* Create the 'current' file. */ |
| SVN_ERR(svn_io_file_create(svn_fs_fs__path_current(dst_fs, pool), |
| (dst_ffd->format >= |
| SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT |
| ? "0\n" : "0 1 1\n"), |
| pool)); |
| |
| /* Create lock file and UUID. */ |
| SVN_ERR(svn_io_file_create(path_lock(dst_fs, pool), "", pool)); |
| SVN_ERR(svn_fs_fs__set_uuid(dst_fs, src_fs->uuid, pool)); |
| |
| /* Create the min unpacked rev file. */ |
| if (dst_ffd->format >= SVN_FS_FS__MIN_PACKED_FORMAT) |
| SVN_ERR(svn_io_file_create(path_min_unpacked_rev(dst_fs, pool), |
| "0\n", pool)); |
| /* Create the txn-current file if the repository supports |
| the transaction sequence file. */ |
| if (dst_ffd->format >= SVN_FS_FS__MIN_TXN_CURRENT_FORMAT) |
| { |
| SVN_ERR(svn_io_file_create(path_txn_current(dst_fs, pool), |
| "0\n", pool)); |
| SVN_ERR(svn_io_file_create(path_txn_current_lock(dst_fs, pool), |
| "", pool)); |
| } |
| |
| dst_ffd->youngest_rev_cache = 0; |
| |
| hotcopy_setup_shared_fs_data(src_fs, dst_fs); |
| SVN_ERR(svn_fs_fs__initialize_caches(dst_fs, 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_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *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) |
| { |
| /* Destination doesn't exist yet. Perform a normal hotcopy to a |
| * empty destination using the same configuration as the source. */ |
| SVN_ERR(hotcopy_create_empty_dest(src_fs, dst_fs, dst_path, pool)); |
| } |
| else |
| { |
| /* Check the existing repository. */ |
| SVN_ERR(svn_fs_fs__open(dst_fs, dst_path, pool)); |
| SVN_ERR(hotcopy_incremental_check_preconditions(src_fs, dst_fs, |
| pool)); |
| hotcopy_setup_shared_fs_data(src_fs, dst_fs); |
| SVN_ERR(svn_fs_fs__initialize_caches(dst_fs, pool)); |
| } |
| } |
| else |
| { |
| /* Start out with an empty destination using the same configuration |
| * as the source. */ |
| SVN_ERR(hotcopy_create_empty_dest(src_fs, dst_fs, dst_path, pool)); |
| } |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| hbb.src_fs = src_fs; |
| hbb.dst_fs = dst_fs; |
| hbb.incremental = incremental; |
| hbb.cancel_func = cancel_func; |
| hbb.cancel_baton = cancel_baton; |
| SVN_ERR(svn_fs_fs__with_write_lock(dst_fs, hotcopy_body, &hbb, pool)); |
| |
| return SVN_NO_ERROR; |
| } |