20640: Merge branch 'main' into 20640-computed-permissions-api 20640-computed-permissions-api
authorTom Clegg <tom@curii.com>
Fri, 28 Jun 2024 23:09:22 +0000 (19:09 -0400)
committerTom Clegg <tom@curii.com>
Fri, 28 Jun 2024 23:09:22 +0000 (19:09 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

65 files changed:
build/package-testing/common-test-rails-server-package.sh
build/rails-package-scripts/postinst.sh
build/run-library.sh
build/run-tests.sh
doc/_includes/_setup_redhat_repo.liquid
doc/api/methods/collections.html.textile.liquid
doc/api/methods/groups.html.textile.liquid
go.mod
go.sum
lib/controller/localdb/collection.go
lib/controller/localdb/collection_test.go
lib/controller/localdb/login_oidc.go
lib/crunchrun/copier.go
lib/crunchrun/copier_test.go
lib/crunchrun/crunchrun.go
lib/crunchrun/crunchrun_test.go
lib/crunchrun/integration_test.go
lib/deduplicationreport/report.go
lib/install/deps.go
sdk/cwl/README.rst
sdk/go/arvadostest/manifest.go [new file with mode: 0644]
sdk/go/arvadostest/oidc_provider.go
sdk/go/blockdigest/blockdigest.go
sdk/go/keepclient/collectionreader.go
sdk/go/manifest/manifest.go [deleted file]
sdk/go/manifest/manifest_test.go [deleted file]
sdk/go/manifest/testdata/long_manifest [deleted file]
sdk/go/manifest/testdata/short_manifest [deleted file]
sdk/python/README.rst
sdk/python/arvados/commands/arv_copy.py
services/api/Gemfile.lock
services/api/app/controllers/arvados/v1/groups_controller.rb
services/api/app/controllers/sys_controller.rb
services/api/app/models/group.rb
services/api/db/migrate/20240618121312_create_uuid_locks.rb [new file with mode: 0644]
services/api/db/structure.sql
services/api/lib/can_be_an_owner.rb
services/api/lib/trashable.rb
services/api/test/fixtures/groups.yml
services/api/test/fixtures/links.yml
services/api/test/functional/arvados/v1/groups_controller_test.rb
services/api/test/functional/sys_controller_test.rb
services/api/test/integration/permissions_test.rb
services/fuse/README.rst
services/workbench2/src/components/subprocess-progress-bar/subprocess-progress-bar.test.tsx
services/workbench2/src/components/subprocess-progress-bar/subprocess-progress-bar.tsx
services/workbench2/src/store/context-menu/context-menu-actions.ts
services/workbench2/src/store/subprocess-panel/subprocess-panel-actions.ts
services/workbench2/src/views-components/context-menu/action-sets/project-action-set.ts
tools/crunchstat-summary/README.rst
tools/salt-install/config_examples/multi_host/aws/pillars/postgresql_external.sls [new file with mode: 0644]
tools/salt-install/config_examples/multi_host/aws/states/postgresql_external.sls [new file with mode: 0644]
tools/salt-install/installer.sh
tools/salt-install/provision.sh
tools/salt-install/terraform/aws/services/locals.tf
tools/salt-install/terraform/aws/services/main.tf
tools/salt-install/terraform/aws/services/outputs.tf
tools/salt-install/terraform/aws/services/terraform.tfvars
tools/salt-install/terraform/aws/services/variables.tf
tools/salt-install/terraform/aws/vpc/locals.tf
tools/salt-install/terraform/aws/vpc/main.tf
tools/salt-install/terraform/aws/vpc/outputs.tf
tools/salt-install/terraform/aws/vpc/terraform.tfvars
tools/salt-install/terraform/aws/vpc/variables.tf
tools/user-activity/README.rst

index 62b30c37d1f0963f0dccc4425c85c7e63a429a5e..df98078de3e4aa4ec254b6a371ec7dcb7e85b0eb 100755 (executable)
@@ -12,7 +12,27 @@ else
     PACKAGE_NAME=$1; shift
 fi
 
-cd "/var/www/${PACKAGE_NAME%-server}/current"
+API_GEMS_LS="$(mktemp --tmpdir api-gems-XXXXXX.list)"
+trap 'rm -f "$API_GEMS_LS"' EXIT INT TERM QUIT
+
+cd "/var/www/${PACKAGE_NAME%-server}"
+
+check_gem_dirs() {
+    local when="$1"; shift
+    env -C shared/vendor_bundle/ruby ls -1 >"$API_GEMS_LS"
+    local ls_count="$(wc -l <"$API_GEMS_LS")"
+    if [ "$ls_count" = 1 ]; then
+        return 0
+    fi
+    echo "Package $PACKAGE_NAME FAILED: $ls_count gem directories created after $when:" >&2
+    case "${ARVADOS_DEBUG:-0}" in
+        0) cat "$API_GEMS_LS" >&2 ;;
+        *) env -C shared/vendor_bundle/ruby find -maxdepth 3 -type d -ls >&2 ;;
+    esac
+    return 11
+}
+
+check_gem_dirs "initial install"
 
 case "$TARGET" in
     debian*|ubuntu*)
@@ -29,4 +49,5 @@ case "$TARGET" in
         ;;
 esac
 
-bundle list >"$ARV_PACKAGES_DIR/$PACKAGE_NAME.gems"
+check_gem_dirs "package reinstall"
+env -C current bundle list >"$ARV_PACKAGES_DIR/$PACKAGE_NAME.gems"
index a0b2513b07d53fb666534d407a87dac745812ba0..1fa7d5f4d3491e63c29bca80a7e9c20b3fb7b1fa 100644 (file)
@@ -7,27 +7,22 @@
 
 set -e
 
-DATABASE_READY=1
-APPLICATION_READY=1
-
-report_not_ready() {
-    local ready_flag="$1"; shift
-    local config_file="$1"; shift
-    if [ "1" != "$ready_flag" ]; then cat >&2 <<EOF
-
-PLEASE NOTE:
-
-The $PACKAGE_NAME package was not configured completely because
-$config_file needs some tweaking.
-Please refer to the documentation at
-<$DOC_URL> for more details.
-
-When $(basename "$config_file") has been modified,
-reconfigure or reinstall this package.
-
-EOF
-    fi
-}
+for DISTRO_FAMILY in $(. /etc/os-release && echo "${ID:-} ${ID_LIKE:-}"); do
+    case "$DISTRO_FAMILY" in
+        debian)
+            RESETUP_CMD="dpkg-reconfigure $PACKAGE_NAME"
+            break ;;
+        rhel)
+            RESETUP_CMD="dnf reinstall $PACKAGE_NAME"
+            break ;;
+    esac
+done
+if [ -z "$RESETUP_CMD" ]; then
+   echo "$PACKAGE_NAME postinst skipped: don't recognize the distribution from /etc/os-release" >&2
+   exit 0
+fi
+# Default documentation URL. This can be set to a more specific URL.
+NOT_READY_DOC_URL="https://doc.arvados.org/install/install-api-server.html"
 
 report_web_service_warning() {
     local warning="$1"; shift
@@ -38,10 +33,8 @@ WARNING: $warning.
 To override, set the WEB_SERVICE environment variable to the name of the service
 hosting the Rails server.
 
-For Debian-based systems, then reconfigure this package with dpkg-reconfigure.
-
-For RPM-based systems, then reinstall this package.
-
+After you do that, resume $PACKAGE_NAME setup by running:
+  $RESETUP_CMD
 EOF
 }
 
@@ -128,14 +121,10 @@ prepare_database() {
       run_and_report "Running db:migrate" \
                      bin/rake db:migrate
   elif echo "$DB_MIGRATE_STATUS" | grep -q 'database .* does not exist'; then
-      if ! run_and_report "Running db:setup" \
-           bin/rake db:setup 2>/dev/null; then
-          echo "Warning: unable to set up database." >&2
-          DATABASE_READY=0
-      fi
+      run_and_report "Running db:setup" bin/rake db:setup
   else
-    echo "Warning: Database is not ready to set up. Skipping database setup." >&2
-    DATABASE_READY=0
+      # We don't have enough configuration to even check the database.
+      return 1
   fi
 }
 
@@ -157,31 +146,14 @@ configure_version() {
         "Multiple web services found.  Choosing the first one ($WEB_SERVICE)"
   fi
 
-  if [ -e /etc/redhat-release ]; then
-      # Recognize any service that starts with "nginx"; e.g., nginx16.
-      if [ "$WEB_SERVICE" != "${WEB_SERVICE#nginx}" ]; then
-        WWW_OWNER=nginx
-      else
-        WWW_OWNER=apache
-      fi
-  else
-      # Assume we're on a Debian-based system for now.
-      # Both Apache and Nginx run as www-data by default.
-      WWW_OWNER=www-data
-  fi
-
-  echo
-  echo "Assumption: $WEB_SERVICE is configured to serve Rails from"
-  echo "            $RELEASE_PATH"
-  echo "Assumption: $WEB_SERVICE and passenger run as $WWW_OWNER"
-  echo
-
-  echo -n "Creating symlinks to configuration in $CONFIG_PATH ..."
-  setup_confdirs /etc/arvados "$CONFIG_PATH"
-  setup_conffile environments/production.rb environments/production.rb.example \
-      || true
-  setup_extra_conffiles
-  echo "... done."
+  case "$DISTRO_FAMILY" in
+      debian) WWW_OWNER=www-data ;;
+      rhel) case "$WEB_SERVICE" in
+                httpd*) WWW_OWNER=apache ;;
+                nginx*) WWW_OWNER=nginx ;;
+            esac
+            ;;
+  esac
 
   # Before we do anything else, make sure some directories and files are in place
   if [ ! -e $SHARED_PATH/log ]; then mkdir -p $SHARED_PATH/log; fi
@@ -193,12 +165,13 @@ configure_version() {
   export RAILS_ENV=production
 
   run_and_report "Installing bundler" gem install --conservative --version '~> 2.4.0' bundler
+  local ruby_minor_ver="$(ruby -e 'puts RUBY_VERSION.split(".")[..1].join(".")')"
   local bundle="$(gem contents --version '~> 2.4.0' bundler | grep -E '/(bin|exe)/bundle$' | tail -n1)"
   if ! [ -x "$bundle" ]; then
       # Some distros (at least Ubuntu 24.04) append the Ruby version to the
       # executable name, but that isn't reflected in the output of
       # `gem contents`. Check for that version.
-      bundle="$bundle$(ruby -e 'puts RUBY_VERSION.split(".")[..1].join(".")')"
+      bundle="$bundle$ruby_minor_ver"
       if ! [ -x "$bundle" ]; then
           echo "Error: failed to find \`bundle\` command after installing bundler gem" >&2
           return 1
@@ -215,56 +188,80 @@ configure_version() {
   find vendor/cache -maxdepth 1 -name '*.gem' -print0 \
       | run_and_report "Installing bundle gems" xargs -0r \
                        gem install --conservative --ignore-dependencies --local --quiet \
-                       --install-dir="$bundle_path/ruby/$(ruby -e 'puts RUBY_VERSION')"
+                       --install-dir="$bundle_path/ruby/$ruby_minor_ver.0"
   run_and_report "Running bundle install" "$bundle" install --prefer-local --quiet
   run_and_report "Verifying bundle is complete" "$bundle" exec true
 
-  echo -n "Ensuring directory and file permissions ..."
-  # Ensure correct ownership of a few files
-  chown "$WWW_OWNER:" $RELEASE_PATH/config/environment.rb
-  chown "$WWW_OWNER:" $RELEASE_PATH/config.ru
-  chown "$WWW_OWNER:" $RELEASE_PATH/Gemfile.lock
-  chown -R "$WWW_OWNER:" $RELEASE_PATH/tmp || true
-  chown -R "$WWW_OWNER:" $SHARED_PATH/log
-  # Make sure postgres doesn't try to use a pager.
-  export PAGER=
-  case "$RAILSPKG_DATABASE_LOAD_TASK" in
-      # db:structure:load was deprecated in Rails 6.1 and shouldn't be used.
-      db:schema:load | db:structure:load)
-          chown "$WWW_OWNER:" $RELEASE_PATH/db/schema.rb || true
-          chown "$WWW_OWNER:" $RELEASE_PATH/db/structure.sql || true
-          ;;
-  esac
-  chmod 644 $SHARED_PATH/log/*
-  chmod -R 2775 $RELEASE_PATH/tmp || true
-  echo "... done."
+  if [ -z "$WWW_OWNER" ]; then
+    NOT_READY_REASON="there is no web service account to own Arvados configuration"
+    NOT_READY_DOC_URL="https://doc.arvados.org/install/nginx.html"
+  else
+    cat <<EOF
+
+Assumption: $WEB_SERVICE is configured to serve Rails from
+            $RELEASE_PATH
+Assumption: $WEB_SERVICE and passenger run as $WWW_OWNER
+
+EOF
 
-  if [ -n "$RAILSPKG_DATABASE_LOAD_TASK" ]; then
-      prepare_database
+    echo -n "Creating symlinks to configuration in $CONFIG_PATH ..."
+    setup_confdirs /etc/arvados "$CONFIG_PATH"
+    setup_conffile environments/production.rb environments/production.rb.example \
+        || true
+    setup_extra_conffiles
+    echo "... done."
+
+    echo -n "Ensuring directory and file permissions ..."
+    # Ensure correct ownership of a few files
+    chown "$WWW_OWNER:" $RELEASE_PATH/config/environment.rb
+    chown "$WWW_OWNER:" $RELEASE_PATH/config.ru
+    chown "$WWW_OWNER:" $RELEASE_PATH/Gemfile.lock
+    chown -R "$WWW_OWNER:" $SHARED_PATH/log
+    # Make sure postgres doesn't try to use a pager.
+    export PAGER=
+    case "$RAILSPKG_DATABASE_LOAD_TASK" in
+        # db:structure:load was deprecated in Rails 6.1 and shouldn't be used.
+        db:schema:load | db:structure:load)
+            chown "$WWW_OWNER:" $RELEASE_PATH/db/schema.rb || true
+            chown "$WWW_OWNER:" $RELEASE_PATH/db/structure.sql || true
+            ;;
+    esac
+    chmod 644 $SHARED_PATH/log/*
+    echo "... done."
   fi
 
-  if [ -e /etc/arvados/config.yml ]; then
-      # warn about config errors (deprecated/removed keys from
-      # previous version, etc)
-      run_and_report "Checking configuration for completeness" \
-                     bin/rake config:check || APPLICATION_READY=0
-  else
-      APPLICATION_READY=0
+  if [ -n "$NOT_READY_REASON" ]; then
+      :
+  # warn about config errors (deprecated/removed keys from
+  # previous version, etc)
+  elif ! run_and_report "Checking configuration for completeness" bin/rake config:check; then
+      NOT_READY_REASON="you must add required configuration settings to /etc/arvados/config.yml"
+      NOT_READY_DOC_URL="https://doc.arvados.org/install/install-api-server.html#update-config"
+  elif [ -z "$RAILSPKG_DATABASE_LOAD_TASK" ]; then
+      :
+  elif ! prepare_database; then
+      NOT_READY_REASON="database setup could not be completed"
   fi
 
-  chown -R "$WWW_OWNER:" $RELEASE_PATH/tmp
+  if [ -n "$WWW_OWNER" ]; then
+    chown -R "$WWW_OWNER:" $RELEASE_PATH/tmp
+    chmod -R 2775 $RELEASE_PATH/tmp
+  fi
 
-  if [ -n "$SERVICE_MANAGER" ]; then
+  if [ -z "$NOT_READY_REASON" ] && [ -n "$SERVICE_MANAGER" ]; then
       service_command "$SERVICE_MANAGER" restart "$WEB_SERVICE"
   fi
 }
 
-if [ "$1" = configure ]; then
-  # This is a debian-based system
-  configure_version
-elif [ "$1" = "0" ] || [ "$1" = "1" ] || [ "$1" = "2" ]; then
-  # This is an rpm-based system
-  configure_version
-fi
+configure_version
+if [ -n "$NOT_READY_REASON" ]; then
+    cat >&2 <<EOF
+NOTE: The $PACKAGE_NAME package was not configured completely because
+$NOT_READY_REASON.
+Please refer to the documentation for next steps:
+  <$NOT_READY_DOC_URL>
 
-report_not_ready "$APPLICATION_READY" "/etc/arvados/config.yml"
+After you do that, resume $PACKAGE_NAME setup by running:
+  $RESETUP_CMD
+EOF
+fi
index 6bbfa36cdeaf5bd7a128423f881e09eb95186291..33ae754037d1c95c5318a9bbd56c6afaa1c0e54d 100755 (executable)
@@ -473,7 +473,7 @@ test_package_presence() {
     else
       local rpm_root
       case "$TARGET" in
-        rocky8) rpm_root="CentOS/8/dev" ;;
+        rocky8) rpm_root="RHEL/8/dev" ;;
         *)
           echo "FIXME: Don't know RPM URL path for $TARGET, building"
           return 0
index 0eb421454e80dca7914b04c67266d563af957853..01708959b17a2a5f70edce99f360ff6984142ce3 100755 (executable)
@@ -113,7 +113,6 @@ sdk/go/dispatch
 sdk/go/keepclient
 sdk/go/health
 sdk/go/httpserver
-sdk/go/manifest
 sdk/go/blockdigest
 sdk/go/asyncbuf
 sdk/go/stats
index 8fd82e88a69bab1707008d0dfc63a5e6a4a01aec..69898caf5464b3add37a2b518557a611d15db0a6 100644 (file)
@@ -24,9 +24,9 @@ Set up the Arvados package repository
 <pre><code># <span class="userinput">tee /etc/yum.repos.d/arvados.repo &gt;/dev/null &lt;&lt;'EOF'
 [arvados]
 name=Arvados
-baseurl=http://rpm.arvados.org/CentOS/$releasever/os/$basearch/
+baseurl=http://rpm.arvados.org/RHEL/$releasever/os/$basearch/
 gpgcheck=1
-gpgkey=http://rpm.arvados.org/CentOS/RPM-GPG-KEY-arvados
+gpgkey=http://rpm.arvados.org/RHEL/RPM-GPG-KEY-arvados
 EOF</span>
 {%- if modules_to_enable != nil %}
 # <span class="userinput">dnf module enable {{ modules_to_enable }}</span>
index 29d28d42a221cd95e6436a1e2c61a6eb173cb2aa..1afb066503107b2d7ceda7ecb1d59a844ecd3262 100644 (file)
@@ -26,10 +26,10 @@ Each collection has, in addition to the "Common resource fields":{{site.baseurl}
 table(table table-bordered table-condensed).
 |_. Attribute|_. Type|_. Description|_. Example|
 |name|string|||
-|description|text|||
+|description|text|Free text description of the group.  May be HTML formatted, must be appropriately sanitized before display.||
 |properties|hash|User-defined metadata, may be used in queries using "subproperty filters":{{site.baseurl}}/api/methods.html#subpropertyfilters ||
 |portable_data_hash|string|The MD5 sum of the manifest text stripped of block hints other than the size hint.||
-|manifest_text|text|||
+|manifest_text|text|The manifest describing how to assemble blocks into files, in the "Arvados manifest format":{{site.baseurl}}/architecture/manifest-format.html||
 |replication_desired|number|Minimum storage replication level desired for each data block referenced by this collection. A value of @null@ signifies that the site default replication level (typically 2) is desired.|@2@|
 |replication_confirmed|number|Replication level most recently confirmed by the storage system. This field is null when a collection is first created, and is reset to null when the manifest_text changes in a way that introduces a new data block. An integer value indicates the replication level of the _least replicated_ data block in the collection.|@2@, null|
 |replication_confirmed_at|datetime|When @replication_confirmed@ was confirmed. If @replication_confirmed@ is null, this field is also null.||
@@ -55,6 +55,69 @@ Referenced blocks are protected from garbage collection in Keep.
 
 Data can be shared with other users via the Arvados permission model.
 
+h3(#trashing). Trashing collections
+
+Collections can be trashed by updating the record and setting the @trash_at@ field, or with the "delete":#delete method.  The delete method sets @trash_at@ to "now".
+
+The value of @trash_at@ can be set to a time in the future as a feature to automatically expire collections.
+
+When @trash_at@ is set, @delete_at@ will also be set.  Normally @delete_at = trash_at + Collections.DefaultTrashLifetime@.  When the @trash_at@ time is past but @delete_at@ is in the future, the trashed collection is invisible to most API calls unless the @include_trash@ parameter is true.  Collections in the trashed state can be "untrashed":#untrash so long as @delete_at@ has not past.  Collections are also trashed if they are contained in a "trashed group":groups.html#trashing
+
+Once @delete_at@ is past, the collection and all of its previous versions will be deleted permanently and can no longer be untrashed.
+
+h3(#replace_files). Using "replace_files" to create/update collections
+
+The @replace_files@ option can be used with the "create":#create and "update":#update APIs to efficiently copy individual files and directory trees from other collections, and copy/rename/delete items within an existing collection, without transferring any file data.
+
+@replace_files@ keys indicate target paths in the new collection, and values specify sources that should be copied to the target paths.
+* Each target path must be an absolute canonical path beginning with @/@. It must not contain @.@ or @..@ components, consecutive @/@ characters, or a trailing @/@ after the final component.
+* Each source must be either an empty string (signifying that the target path is to be deleted), or @PDH/path@ where @PDH@ is the portable data hash of a collection on the cluster and @/path@ is a file or directory in that collection.
+* In an @update@ request, sources may reference the current portable data hash of the collection being updated.
+
+Example: delete @foo.txt@ from a collection
+
+<notextile><pre>
+"replace_files": {
+  "/foo.txt": ""
+}
+</pre></notextile>
+
+Example: rename @foo.txt@ to @bar.txt@ in a collection with portable data hash @fa7aeb5140e2848d39b416daeef4ffc5+45@
+
+<notextile><pre>
+"replace_files": {
+  "/foo.txt": "",
+  "/bar.txt": "fa7aeb5140e2848d39b416daeef4ffc5+45/foo.txt"
+}
+</pre></notextile>
+
+Example: delete current contents, then add content from multiple collections
+
+<notextile><pre>
+"replace_files": {
+  "/": "",
+  "/copy of collection 1": "1f4b0bc7583c2a7f9102c395f4ffc5e3+45/",
+  "/copy of collection 2": "ea10d51bcf88862dbcc36eb292017dfd+45/"
+}
+</pre></notextile>
+
+Example: replace entire collection with a copy of a subdirectory from another collection
+
+<notextile><pre>
+"replace_files": {
+  "/": "1f4b0bc7583c2a7f9102c395f4ffc5e3+45/subdir"
+}
+</pre></notextile>
+
+A target path with a non-empty source cannot be the ancestor of another target path in the same request. For example, the following request is invalid:
+
+<notextile><pre>
+"replace_files": {
+  "/foo": "fa7aeb5140e2848d39b416daeef4ffc5+45/",
+  "/foo/this_will_return_an_error": ""
+}
+</pre></notextile>
+
 h2. Methods
 
 See "Common resource methods":{{site.baseurl}}/api/methods.html for more information about @create@, @delete@, @get@, @list@, and @update@.
@@ -63,7 +126,7 @@ Required arguments are displayed in %{background:#ccffcc}green%.
 
 Supports federated @get@ only, which may be called with either a uuid or a portable data hash.  When requesting a portable data hash which is not available on the home cluster, the query is forwarded to all the clusters listed in @RemoteClusters@ and returns the first successful result.
 
-h3. create
+h3(#create). create
 
 Create a new Collection.
 
@@ -76,7 +139,7 @@ table(table table-bordered table-condensed).
 
 The new collection's content can be initialized by providing a @manifest_text@ key in the provided @collection@ object, or by using the @replace_files@ option (see "replace_files":#replace_files below).
 
-h3. delete
+h3(#delete). delete
 
 Put a Collection in the trash.  This sets the @trash_at@ field to @now@ and @delete_at@ field to @now@ + token TTL.  A trashed collection is invisible to most API calls unless the @include_trash@ parameter is true.
 
@@ -139,7 +202,7 @@ As a workaround, you can search for both the directory path and file name separa
 filters: [["file_names", "ilike", "%dir1/dir2/dir3%"], ["file_names", "ilike", "%sample1234.fastq%"]]
 </pre>
 
-h3. update
+h3(#update). update
 
 Update attributes of an existing Collection.
 
@@ -153,7 +216,7 @@ table(table table-bordered table-condensed).
 
 The collection's content can be updated by providing a @manifest_text@ key in the provided @collection@ object, or by using the @replace_files@ option (see "replace_files":#replace_files below).
 
-h3. untrash
+h3(#untrash). untrash
 
 Remove a Collection from the trash.  This sets the @trash_at@ and @delete_at@ fields to @null@.
 
@@ -196,56 +259,3 @@ Arguments:
 table(table table-bordered table-condensed).
 |_. Argument |_. Type |_. Description |_. Location |_. Example |
 {background:#ccffcc}.|uuid|string|The UUID of the Collection to get usage.|path||
-
-h2(#replace_files). Using "replace_files" to create/update collections
-
-The @replace_files@ option can be used with the @create@ and @update@ APIs to efficiently copy individual files and directory trees from other collections, and copy/rename/delete items within an existing collection, without transferring any file data.
-
-@replace_files@ keys indicate target paths in the new collection, and values specify sources that should be copied to the target paths.
-* Each target path must be an absolute canonical path beginning with @/@. It must not contain @.@ or @..@ components, consecutive @/@ characters, or a trailing @/@ after the final component.
-* Each source must be either an empty string (signifying that the target path is to be deleted), or @PDH/path@ where @PDH@ is the portable data hash of a collection on the cluster and @/path@ is a file or directory in that collection.
-* In an @update@ request, sources may reference the current portable data hash of the collection being updated.
-
-Example: delete @foo.txt@ from a collection
-
-<notextile><pre>
-"replace_files": {
-  "/foo.txt": ""
-}
-</pre></notextile>
-
-Example: rename @foo.txt@ to @bar.txt@ in a collection with portable data hash @fa7aeb5140e2848d39b416daeef4ffc5+45@
-
-<notextile><pre>
-"replace_files": {
-  "/foo.txt": "",
-  "/bar.txt": "fa7aeb5140e2848d39b416daeef4ffc5+45/foo.txt"
-}
-</pre></notextile>
-
-Example: delete current contents, then add content from multiple collections
-
-<notextile><pre>
-"replace_files": {
-  "/": "",
-  "/copy of collection 1": "1f4b0bc7583c2a7f9102c395f4ffc5e3+45/",
-  "/copy of collection 2": "ea10d51bcf88862dbcc36eb292017dfd+45/"
-}
-</pre></notextile>
-
-Example: replace entire collection with a copy of a subdirectory from another collection
-
-<notextile><pre>
-"replace_files": {
-  "/": "1f4b0bc7583c2a7f9102c395f4ffc5e3+45/subdir"
-}
-</pre></notextile>
-
-A target path with a non-empty source cannot be the ancestor of another target path in the same request. For example, the following request is invalid:
-
-<notextile><pre>
-"replace_files": {
-  "/foo": "fa7aeb5140e2848d39b416daeef4ffc5+45/",
-  "/foo/this_will_return_an_error": ""
-}
-</pre></notextile>
index bc49ad68d5d85ea9435851253fad4d9beafa1918..e724fe82e789470bab398e4409811f49f04441d0 100644 (file)
@@ -28,27 +28,29 @@ table(table table-bordered table-condensed).
 |group_class|string|Type of group. @project@ and @filter@ indicate that the group should be displayed by Workbench and arv-mount as a project for organizing and naming objects. @role@ is used as part of the "permission system":{{site.baseurl}}/api/permission-model.html. |@"filter"@
 @"project"@
 @"role"@|
-|description|text|||
+|description|text|Free text description of the group.  May be HTML formatted, must be appropriately sanitized before display.||
 |properties|hash|User-defined metadata, may be used in queries using "subproperty filters":{{site.baseurl}}/api/methods.html#subpropertyfilters ||
 |writable_by|array|(Deprecated) List of UUID strings identifying Users and other Groups that have write permission for this Group.  Users who are allowed to administer the Group will receive a list of user/group UUIDs that have permission via explicit permission links; permissions via parent/ancestor groups are not taken into account.  Other users will receive a partial list including only the Group's owner_uuid and (if applicable) their own user UUID.||
 |can_write|boolean|True if the current user has write permission on this group.||
 |can_manage|boolean|True if the current user has manage permission on this group.||
-|trash_at|datetime|If @trash_at@ is non-null and in the past, this group and all objects directly or indirectly owned by the group will be hidden from API calls.  May be untrashed.||
+|trash_at|datetime|If @trash_at@ is non-null and in the past, this group and all objects directly or indirectly owned by the group will be hidden from API calls.  May be untrashed as long as @delete_at@ is in the future.||
 |delete_at|datetime|If @delete_at@ is non-null and in the past, the group and all objects directly or indirectly owned by the group may be permanently deleted.||
 |is_trashed|datetime|True if @trash_at@ is in the past, false if not.||
 |frozen_by_uuid|string|For a frozen project, indicates the user who froze the project; null in all other cases. When a project is frozen, no further changes can be made to the project or its contents, even by admins. Attempting to add new items or modify, rename, move, trash, or delete the project or its contents, including any subprojects, will return an error.||
 
-h3(#frozen). Frozen projects
+h2. Group types and states
 
-A user with @manage@ permission can set the @frozen_by_uuid@ attribute of a @project@ group to their own user UUID. Once this is done, no further changes can be made to the project or its contents, including subprojects.
+h3(#project). Project groups
 
-The @frozen_by_uuid@ attribute can be cleared by an admin user. It can also be cleared by a user with @manage@ permission, unless the @API.UnfreezeProjectRequiresAdmin@ configuration setting is active.
+Groups with @group_class: project@ are used to organize objects and subprojects through ownership.  When "trashed or deleted":#trashing, all items owned by the project (including subprojects, collections, or container requests) as well as permissions (permission links) granted to the project are also trashed or deleted.
 
-The optional @API.FreezeProjectRequiresDescription@ and @API.FreezeProjectRequiresProperties@ configuration settings can be used to prevent users from freezing projects that have empty @description@ and/or specified @properties@ entries.
+h3(#role). Role groups
+
+Groups with @group_class: role@ are used to grant permissions to users (or other groups) through permission links.  Role groups can confer "can_manage" permission but cannot directly own objects.  When "trashed and deleted":#trashing group membership and permission grants (expressed as permission links) are deleted as well.
 
 h3(#filter). Filter groups
 
-@filter@ groups are virtual groups; they can not own other objects. Filter groups have a special @properties@ field named @filters@, which must be an array of filter conditions. See "list method filters":{{site.baseurl}}/api/methods.html#filters for details on the syntax of valid filters, but keep in mind that the attributes must include the object type (@collections@, @container_requests@, @groups@, @workflows@), separated with a dot from the field to be filtered on.
+Groups with @group_class: filter@ groups are virtual groups; they can not own other objects, but instead their contents (as returned by the "contents":#contents API method) are defined by a query. Filter groups have a special @properties@ field named @filters@, which must be an array of filter conditions. See "list method filters":{{site.baseurl}}/api/methods.html#filters for details on the syntax of valid filters, but keep in mind that the attributes must include the object type (@collections@, @container_requests@, @groups@, @workflows@), separated with a dot from the field to be filtered on.
 
 Filters are applied with an implied *and* between them, but each filter only applies to the object type specified. The results are subject to the usual access controls - they are a subset of all objects the user can see. Here is an example:
 
@@ -93,6 +95,28 @@ The 'is_a' filter operator is of particular interest to limit the @filter@ group
  },
  </pre>
 
+"Trashed or deleting":#trashing a filter group causes the group itself to be hidden or deleted, but has no effect on the items returned in "contents", i.e. the database objects in "contents" are not hidden or deleted and may be accessed by other means.
+
+h3(#trashing). Trashing groups
+
+Groups can be trashed by updating the record and setting the @trash_at@ field, or with the "delete":#delete method.  The delete method sets @trash_at@ to "now".
+
+The value of @trash_at@ can be set to a time in the future as a feature to automatically expire groups.
+
+When @trash_at@ is set, @delete_at@ will also be set.  Normally @delete_at = trash_at + Collections.DefaultTrashLifetime@ for projects and filter groups, and @delete_at = trash_at@ for role groups.  When the @trash_at@ time is past but @delete_at@ is in the future, the trashed group is invisible to most API calls unless the @include_trash@ parameter is true.  All objects directly or indirectly owned by the group (including subprojects, collections, or container requests) are considered trashed as well.  Groups in the trashed state can be "untrashed":#untrash so long as @delete_at@ has not past.
+
+Once @delete_at@ is past, the group will be deleted permanently and can no longer be untrashed.  Different group types have different behavior when deleted, described above.
+
+Note: like other groups, "role" groups may have @trash_at@ set to date in the future, however roles groups are required to have @delete_at = trash_at@, so the trash time and delete time expire at the same time.  This means once @trash_at@ expires the role group is deleted immediately.  Role groups with @trash_at@ set can only be "untrashed":#untrash before they expire.
+
+h3(#frozen). Frozen projects
+
+A user with @manage@ permission can set the @frozen_by_uuid@ attribute of a @project@ group to their own user UUID. Once this is done, no further changes can be made to the project or its contents, including subprojects.
+
+The @frozen_by_uuid@ attribute can be cleared by an admin user. It can also be cleared by a user with @manage@ permission, unless the @API.UnfreezeProjectRequiresAdmin@ configuration setting is active.
+
+The optional @API.FreezeProjectRequiresDescription@ and @API.FreezeProjectRequiresProperties@ configuration settings can be used to prevent users from freezing projects that have empty @description@ and/or empty @properties@ entries.
+
 h2. Methods
 
 See "Common resource methods":{{site.baseurl}}/api/methods.html for more information about @create@, @delete@, @get@, @list@, and @update@.
@@ -141,9 +165,9 @@ table(table table-bordered table-condensed).
 |group|object||query||
 |async|boolean (default false)|Defer the permissions graph update by a configured number of seconds. (By default, @async_permissions_update_interval@ is 20 seconds). On success, the response is 202 (Accepted).|query|@true@|
 
-h3. delete
+h3(#delete). delete
 
-Put a Group in the trash.  This sets the @trash_at@ field to @now@ and @delete_at@ field to @now@ + token TTL.  A trashed group is invisible to most API calls unless the @include_trash@ parameter is true.  All objects directly or indirectly owned by the Group are considered trashed as well.
+Put a Group in the trash.  See "Trashing groups":#trashing for details.
 
 Arguments:
 
@@ -189,9 +213,9 @@ table(table table-bordered table-condensed).
 |group|object||query||
 |async|boolean (default false)|Defer the permissions graph update by a configured number of seconds. (By default, @async_permissions_update_interval@ is 20 seconds). On success, the response is 202 (Accepted).|query|@true@|
 
-h3. untrash
+h3(#untrash). untrash
 
-Remove a Group from the trash.  This sets the @trash_at@ and @delete_at@ fields to @null@.
+Remove a Group from the trash.  Only valid when @delete_at@ is in the future.  This sets the @trash_at@ and @delete_at@ fields to @null@.
 
 Arguments:
 
diff --git a/go.mod b/go.mod
index 9cf9ee187ab72b07d306781e40e5117b7c089aa6..a2a19f458de008092ed7207731ae9a23d61571b4 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -33,7 +33,7 @@ require (
        github.com/gogo/protobuf v1.3.2
        github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
        github.com/gorilla/mux v1.8.0
-       github.com/hashicorp/go-retryablehttp v0.7.6
+       github.com/hashicorp/go-retryablehttp v0.7.7
        github.com/hashicorp/golang-lru v1.0.2
        github.com/hashicorp/yamux v0.1.1
        github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff
@@ -52,7 +52,7 @@ require (
        golang.org/x/sys v0.20.0
        google.golang.org/api v0.181.0
        gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
-       gopkg.in/square/go-jose.v2 v2.6.0
+       gopkg.in/go-jose/go-jose.v2 v2.6.3
        rsc.io/getopt v0.0.0-20170811000552-20be20937449
 )
 
diff --git a/go.sum b/go.sum
index 1db0673efab68e5cc1c0712d0587ca3346af65e1..52a54a1c4f76bbeda2ee321c36378141670c272f 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -206,8 +206,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
 github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
-github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=
-github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
+github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
@@ -451,9 +451,9 @@ gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUy
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
+gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
 gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
-gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
-gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
index 581595e5e3818a56b4194adc47834e87035a3ce8..bfc5c8a42629118bdc8ade3847e09842b2bb90da 100644 (file)
@@ -13,6 +13,7 @@ import (
        "strings"
        "time"
 
+       "git.arvados.org/arvados.git/lib/ctrlctx"
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/arvadosclient"
        "git.arvados.org/arvados.git/sdk/go/auth"
@@ -96,6 +97,10 @@ func (conn *Conn) CollectionUpdate(ctx context.Context, opts arvados.UpdateOptio
                // them.
                opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
        }
+       err = conn.lockUUID(ctx, opts.UUID)
+       if err != nil {
+               return arvados.Collection{}, err
+       }
        if opts.Attrs, err = conn.applyReplaceFilesOption(ctx, opts.UUID, opts.Attrs, opts.ReplaceFiles); err != nil {
                return arvados.Collection{}, err
        }
@@ -126,6 +131,18 @@ func (conn *Conn) signCollection(ctx context.Context, coll *arvados.Collection)
        coll.ManifestText = arvados.SignManifest(coll.ManifestText, token, exp, ttl, []byte(conn.cluster.Collections.BlobSigningKey))
 }
 
+func (conn *Conn) lockUUID(ctx context.Context, uuid string) error {
+       tx, err := ctrlctx.CurrentTx(ctx)
+       if err != nil {
+               return err
+       }
+       _, err = tx.ExecContext(ctx, `insert into uuid_locks (uuid) values ($1) on conflict (uuid) do update set n=uuid_locks.n+1`, uuid)
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
 // If replaceFiles is non-empty, populate attrs["manifest_text"] by
 // starting with the content of fromUUID (or an empty collection if
 // fromUUID is empty) and applying the specified file/directory
index 7d1a909a6fdc4c7135caf6e57a235f9a32be927e..6d7a528eb9688559ab300e4f57c3878ea5396385 100644 (file)
@@ -11,6 +11,7 @@ import (
        "sort"
        "strconv"
        "strings"
+       "sync"
        "time"
 
        "git.arvados.org/arvados.git/lib/ctrlctx"
@@ -246,6 +247,52 @@ func (s *CollectionSuite) expectFiles(c *check.C, coll arvados.Collection, expec
        c.Check(found, check.DeepEquals, expected)
 }
 
+// Until #21701 it's hard to test from the outside whether the
+// uuid_lock mechanism is effectively serializing concurrent
+// replace_files updates to a single collection.  For now, we're
+// really just checking that it doesn't cause updates to deadlock or
+// anything like that.
+func (s *CollectionSuite) TestCollectionUpdateLock(c *check.C) {
+       adminctx := ctrlctx.NewWithToken(s.ctx, s.cluster, arvadostest.AdminToken)
+       foo, err := s.localdb.railsProxy.CollectionCreate(adminctx, arvados.CreateOptions{
+               Attrs: map[string]interface{}{
+                       "owner_uuid":    arvadostest.ActiveUserUUID,
+                       "manifest_text": ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt\n",
+               }})
+       c.Assert(err, check.IsNil)
+       dst, err := s.localdb.CollectionCreate(s.userctx, arvados.CreateOptions{
+               ReplaceFiles: map[string]string{
+                       "/foo.txt": foo.PortableDataHash + "/foo.txt",
+               },
+               Attrs: map[string]interface{}{
+                       "owner_uuid": arvadostest.ActiveUserUUID,
+               }})
+       c.Assert(err, check.IsNil)
+       s.expectFiles(c, dst, "foo.txt")
+
+       var wg sync.WaitGroup
+       for i := 0; i < 10; i++ {
+               name1, name2 := "a", "b"
+               if i&1 == 1 {
+                       name1, name2 = "b", "a"
+               }
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       upd, err := s.localdb.CollectionUpdate(s.userctx, arvados.UpdateOptions{
+                               UUID: dst.UUID,
+                               ReplaceFiles: map[string]string{
+                                       "/" + name1: foo.PortableDataHash + "/foo.txt",
+                                       "/" + name2: "",
+                                       "/foo.txt":  "",
+                               }})
+                       c.Assert(err, check.IsNil)
+                       s.expectFiles(c, upd, name1)
+               }()
+       }
+       wg.Wait()
+}
+
 func (s *CollectionSuite) TestSignatures(c *check.C) {
        resp, err := s.localdb.CollectionGet(s.userctx, arvados.GetOptions{UUID: arvadostest.FooCollection})
        c.Check(err, check.IsNil)
index d91cdddc018f42f02a720d60189fec52a6de385f..06ea09c4cce2d216de7d93796c140e88557dee26 100644 (file)
@@ -38,7 +38,7 @@ import (
        "golang.org/x/oauth2"
        "google.golang.org/api/option"
        "google.golang.org/api/people/v1"
-       "gopkg.in/square/go-jose.v2/jwt"
+       "gopkg.in/go-jose/go-jose.v2/jwt"
 )
 
 var (
index b411948733cf79d21d6e463ee84ff1d130b99a02..b26562a72c5386bd47cf6b0671a6672af460d05c 100644 (file)
@@ -17,7 +17,6 @@ import (
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/keepclient"
-       "git.arvados.org/arvados.git/sdk/go/manifest"
        "github.com/bmatcuk/doublestar/v4"
 )
 
@@ -42,8 +41,8 @@ type filetodo struct {
 // copied from the local filesystem.
 //
 // Symlinks to mounted collections, and any collections mounted under
-// ctrOutputDir, are copied by transforming the relevant parts of the
-// existing manifests, without moving any data around.
+// ctrOutputDir, are copied by reference, without moving any data
+// around.
 //
 // Symlinks to other parts of the container's filesystem result in
 // errors.
@@ -62,37 +61,40 @@ type copier struct {
        secretMounts  map[string]arvados.Mount
        logger        printfer
 
-       dirs     []string
-       files    []filetodo
-       manifest string
+       dirs   []string
+       files  []filetodo
+       staged arvados.CollectionFileSystem
 
-       manifestCache map[string]*manifest.Manifest
+       manifestCache map[string]string
 }
 
 // Copy copies data as needed, and returns a new manifest.
+//
+// Copy should not be called more than once.
 func (cp *copier) Copy() (string, error) {
-       err := cp.walkMount("", cp.ctrOutputDir, limitFollowSymlinks, true)
+       var err error
+       cp.staged, err = (&arvados.Collection{}).FileSystem(cp.client, cp.keepClient)
        if err != nil {
-               return "", fmt.Errorf("error scanning files to copy to output: %v", err)
+               return "", fmt.Errorf("error creating Collection.FileSystem: %v", err)
        }
-       collfs, err := (&arvados.Collection{ManifestText: cp.manifest}).FileSystem(cp.client, cp.keepClient)
+       err = cp.walkMount("", cp.ctrOutputDir, limitFollowSymlinks, true)
        if err != nil {
-               return "", fmt.Errorf("error creating Collection.FileSystem: %v", err)
+               return "", fmt.Errorf("error scanning files to copy to output: %v", err)
        }
 
-       // Remove files/dirs that don't match globs (the ones that
-       // were added during cp.walkMount() by copying subtree
-       // manifests into cp.manifest).
-       err = cp.applyGlobsToCollectionFS(collfs)
+       // Remove files/dirs that don't match globs (the files/dirs
+       // that were added during cp.walkMount() by copying subtree
+       // manifests into cp.staged).
+       err = cp.applyGlobsToStaged()
        if err != nil {
                return "", fmt.Errorf("error while removing non-matching files from output collection: %w", err)
        }
-       // Remove files/dirs that don't match globs (the ones that are
-       // stored on the local filesystem and would need to be copied
-       // in copyFile() below).
+       // Remove files/dirs that don't match globs (the files/dirs
+       // that are stored on the local filesystem and would need to
+       // be copied in copyFile() below).
        cp.applyGlobsToFilesAndDirs()
        for _, d := range cp.dirs {
-               err = collfs.Mkdir(d, 0777)
+               err = cp.staged.Mkdir(d, 0777)
                if err != nil && err != os.ErrExist {
                        return "", fmt.Errorf("error making directory %q in output collection: %v", d, err)
                }
@@ -107,20 +109,20 @@ func (cp *copier) Copy() (string, error) {
                // open so f's data can be packed with it).
                dir, _ := filepath.Split(f.dst)
                if dir != lastparentdir || unflushed > keepclient.BLOCKSIZE {
-                       if err := collfs.Flush("/"+lastparentdir, dir != lastparentdir); err != nil {
+                       if err := cp.staged.Flush("/"+lastparentdir, dir != lastparentdir); err != nil {
                                return "", fmt.Errorf("error flushing output collection file data: %v", err)
                        }
                        unflushed = 0
                }
                lastparentdir = dir
 
-               n, err := cp.copyFile(collfs, f)
+               n, err := cp.copyFile(cp.staged, f)
                if err != nil {
                        return "", fmt.Errorf("error copying file %q into output collection: %v", f, err)
                }
                unflushed += n
        }
-       return collfs.MarshalManifest(".")
+       return cp.staged.MarshalManifest(".")
 }
 
 func (cp *copier) matchGlobs(path string, isDir bool) bool {
@@ -211,15 +213,15 @@ func (cp *copier) applyGlobsToFilesAndDirs() {
        cp.files = keepfiles
 }
 
-// Delete files in collfs that do not match cp.globs.  Also delete
+// Delete files in cp.staged that do not match cp.globs.  Also delete
 // directories that are empty (after deleting non-matching files) and
 // do not match cp.globs themselves.
-func (cp *copier) applyGlobsToCollectionFS(collfs arvados.CollectionFileSystem) error {
+func (cp *copier) applyGlobsToStaged() error {
        if len(cp.globs) == 0 {
                return nil
        }
        include := make(map[string]bool)
-       err := fs.WalkDir(arvados.FS(collfs), "", func(path string, ent fs.DirEntry, err error) error {
+       err := fs.WalkDir(arvados.FS(cp.staged), "", func(path string, ent fs.DirEntry, err error) error {
                if cp.matchGlobs(path, ent.IsDir()) {
                        for i, c := range path {
                                if i > 0 && c == '/' {
@@ -233,12 +235,12 @@ func (cp *copier) applyGlobsToCollectionFS(collfs arvados.CollectionFileSystem)
        if err != nil {
                return err
        }
-       err = fs.WalkDir(arvados.FS(collfs), "", func(path string, ent fs.DirEntry, err error) error {
+       err = fs.WalkDir(arvados.FS(cp.staged), "", func(path string, ent fs.DirEntry, err error) error {
                if err != nil || path == "" {
                        return err
                }
                if !include[path] {
-                       err := collfs.RemoveAll(path)
+                       err := cp.staged.RemoveAll(path)
                        if err != nil {
                                return err
                        }
@@ -307,7 +309,7 @@ func (cp *copier) copyFile(fs arvados.CollectionFileSystem, f filetodo) (int64,
        return n, dst.Close()
 }
 
-// Append to cp.manifest, cp.files, and cp.dirs so as to copy src (an
+// Add to cp.staged, cp.files, and cp.dirs so as to copy src (an
 // absolute path in the container's filesystem) to dest (an absolute
 // path in the output collection, or "" for output root).
 //
@@ -370,7 +372,10 @@ func (cp *copier) walkMount(dest, src string, maxSymlinks int, walkMountsBelow b
                if err != nil {
                        return err
                }
-               cp.manifest += mft.Extract(srcRelPath, dest).Text
+               err = cp.copyFromCollection(dest, &arvados.Collection{ManifestText: mft}, srcRelPath)
+               if err != nil {
+                       return err
+               }
        default:
                cp.logger.Printf("copying %q", outputRelPath)
                hostRoot, err := cp.hostRoot(srcRoot)
@@ -387,8 +392,10 @@ func (cp *copier) walkMount(dest, src string, maxSymlinks int, walkMountsBelow b
                if err != nil {
                        return err
                }
-               mft := manifest.Manifest{Text: coll.ManifestText}
-               cp.manifest += mft.Extract(srcRelPath, dest).Text
+               err = cp.copyFromCollection(dest, &coll, srcRelPath)
+               if err != nil {
+                       return err
+               }
        }
        if walkMountsBelow {
                return cp.walkMountsBelow(dest, src)
@@ -396,6 +403,27 @@ func (cp *copier) walkMount(dest, src string, maxSymlinks int, walkMountsBelow b
        return nil
 }
 
+func (cp *copier) copyFromCollection(dest string, coll *arvados.Collection, srcRelPath string) error {
+       tmpfs, err := coll.FileSystem(cp.client, cp.keepClient)
+       if err != nil {
+               return err
+       }
+       snap, err := arvados.Snapshot(tmpfs, srcRelPath)
+       if err != nil {
+               return err
+       }
+       // Create ancestors of dest, if necessary.
+       for i, c := range dest {
+               if i > 0 && c == '/' {
+                       err = cp.staged.Mkdir(dest[:i], 0777)
+                       if err != nil && !os.IsExist(err) {
+                               return err
+                       }
+               }
+       }
+       return arvados.Splice(cp.staged, dest, snap)
+}
+
 func (cp *copier) walkMountsBelow(dest, src string) error {
        for mnt, mntinfo := range cp.mounts {
                if !strings.HasPrefix(mnt, src+"/") {
@@ -550,20 +578,18 @@ func (cp *copier) copyRegularFiles(m arvados.Mount) bool {
        return m.Kind == "text" || m.Kind == "json" || (m.Kind == "collection" && m.Writable)
 }
 
-func (cp *copier) getManifest(pdh string) (*manifest.Manifest, error) {
+func (cp *copier) getManifest(pdh string) (string, error) {
        if mft, ok := cp.manifestCache[pdh]; ok {
                return mft, nil
        }
        var coll arvados.Collection
        err := cp.client.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+pdh, nil, nil)
        if err != nil {
-               return nil, fmt.Errorf("error retrieving collection record for %q: %s", pdh, err)
+               return "", fmt.Errorf("error retrieving collection record for %q: %s", pdh, err)
        }
-       mft := &manifest.Manifest{Text: coll.ManifestText}
        if cp.manifestCache == nil {
-               cp.manifestCache = map[string]*manifest.Manifest{pdh: mft}
-       } else {
-               cp.manifestCache[pdh] = mft
+               cp.manifestCache = make(map[string]string)
        }
-       return mft, nil
+       cp.manifestCache[pdh] = coll.ManifestText
+       return coll.ManifestText, nil
 }
index 486bf6fa635784eedc78dc326ad0f4dabaf7144e..9ae71b9a22ced61a948f4ad90117b0ba0f78442b 100644 (file)
@@ -6,6 +6,8 @@ package crunchrun
 
 import (
        "bytes"
+       "encoding/json"
+       "fmt"
        "io"
        "io/fs"
        "os"
@@ -13,7 +15,9 @@ import (
        "syscall"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/arvadosclient"
        "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       "git.arvados.org/arvados.git/sdk/go/keepclient"
        "github.com/sirupsen/logrus"
        check "gopkg.in/check.v1"
 )
@@ -28,8 +32,17 @@ type copierSuite struct {
 func (s *copierSuite) SetUpTest(c *check.C) {
        tmpdir := c.MkDir()
        s.log = bytes.Buffer{}
+
+       cl, err := arvadosclient.MakeArvadosClient()
+       c.Assert(err, check.IsNil)
+       kc, err := keepclient.MakeKeepClient(cl)
+       c.Assert(err, check.IsNil)
+       collfs, err := (&arvados.Collection{}).FileSystem(arvados.NewClientFromEnv(), kc)
+       c.Assert(err, check.IsNil)
+
        s.cp = copier{
                client:        arvados.NewClientFromEnv(),
+               keepClient:    kc,
                hostOutputDir: tmpdir,
                ctrOutputDir:  "/ctr/outdir",
                mounts: map[string]arvados.Mount{
@@ -39,6 +52,7 @@ func (s *copierSuite) SetUpTest(c *check.C) {
                        "/secret_text": {Kind: "text", Content: "xyzzy"},
                },
                logger: &logrus.Logger{Out: &s.log, Formatter: &logrus.TextFormatter{}, Level: logrus.InfoLevel},
+               staged: collfs,
        }
 }
 
@@ -137,7 +151,16 @@ func (s *copierSuite) TestSymlinkToMountedCollection(c *check.C) {
 
        err = s.cp.walkMount("", s.cp.ctrOutputDir, 10, true)
        c.Check(err, check.IsNil)
-       c.Check(s.cp.manifest, check.Matches, `(?ms)\./l_dir acbd\S+ 0:3:foo\n\. acbd\S+ 0:3:l_file\n\. 37b5\S+ 0:3:l_file_w\n`)
+       s.checkStagedFile(c, "l_dir/foo", 3)
+       s.checkStagedFile(c, "l_file", 3)
+       s.checkStagedFile(c, "l_file_w", 3)
+}
+
+func (s *copierSuite) checkStagedFile(c *check.C, path string, size int64) {
+       fi, err := s.cp.staged.Stat(path)
+       if c.Check(err, check.IsNil) {
+               c.Check(fi.Size(), check.Equals, size)
+       }
 }
 
 func (s *copierSuite) TestSymlink(c *check.C) {
@@ -286,6 +309,58 @@ func (s *copierSuite) TestSubtreeCouldMatch(c *check.C) {
        }
 }
 
+func (s *copierSuite) TestCopyFromLargeCollection_Readonly(c *check.C) {
+       s.testCopyFromLargeCollection(c, false)
+}
+
+func (s *copierSuite) TestCopyFromLargeCollection_Writable(c *check.C) {
+       s.testCopyFromLargeCollection(c, true)
+}
+
+func (s *copierSuite) testCopyFromLargeCollection(c *check.C, writable bool) {
+       bindtmp := c.MkDir()
+       mtxt := arvadostest.FakeManifest(100, 100, 2, 4<<20)
+       pdh := arvados.PortableDataHash(mtxt)
+       json, err := json.Marshal(arvados.Collection{ManifestText: mtxt, PortableDataHash: pdh})
+       c.Assert(err, check.IsNil)
+       err = os.WriteFile(bindtmp+"/.arvados#collection", json, 0644)
+       // This symlink tricks walkHostFS into calling walkMount on
+       // the fakecollection dir. If we did the obvious thing instead
+       // (i.e., mount a collection under the output dir) walkMount
+       // would see that our fakecollection dir is actually a regular
+       // directory, conclude that the mount has been deleted and
+       // replaced by a regular directory tree, and process the tree
+       // as regular files, bypassing the manifest-copying code path
+       // we're trying to test.
+       err = os.Symlink("/fakecollection", s.cp.hostOutputDir+"/fakecollection")
+       c.Assert(err, check.IsNil)
+       s.cp.mounts["/fakecollection"] = arvados.Mount{
+               Kind:             "collection",
+               PortableDataHash: pdh,
+               Writable:         writable,
+       }
+       s.cp.bindmounts = map[string]bindmount{
+               "/fakecollection": bindmount{HostPath: bindtmp, ReadOnly: !writable},
+       }
+       s.cp.manifestCache = map[string]string{pdh: mtxt}
+       err = s.cp.walkMount("", s.cp.ctrOutputDir, 10, true)
+       c.Check(err, check.IsNil)
+       c.Log(s.log.String())
+
+       // Check some files to ensure they were copied properly.
+       // Specifically, arbitrarily check every 17th file in every
+       // 13th dir.  (This is better than checking all of the files
+       // only in that it's less likely to show up as a distracting
+       // signal in CPU profiling.)
+       for i := 0; i < 100; i += 13 {
+               for j := 0; j < 100; j += 17 {
+                       fnm := fmt.Sprintf("/fakecollection/dir%d/dir%d/file%d", i, j, j)
+                       _, err := s.cp.staged.Stat(fnm)
+                       c.Assert(err, check.IsNil, check.Commentf("%s", fnm))
+               }
+       }
+}
+
 func (s *copierSuite) TestMountBelowExcludedByGlob(c *check.C) {
        bindtmp := c.MkDir()
        s.cp.mounts["/ctr/outdir/include/includer"] = arvados.Mount{
@@ -344,8 +419,10 @@ func (s *copierSuite) TestMountBelowExcludedByGlob(c *check.C) {
        c.Check(s.cp.files, check.DeepEquals, []filetodo{
                {src: s.cp.hostOutputDir + "/include/includew/foo", dst: "/include/includew/foo", size: 3},
        })
-       c.Check(s.cp.manifest, check.Matches, `(?ms).*\./include/includer .*`)
-       c.Check(s.cp.manifest, check.Not(check.Matches), `(?ms).*exclude.*`)
+       manifest, err := s.cp.staged.MarshalManifest(".")
+       c.Assert(err, check.IsNil)
+       c.Check(manifest, check.Matches, `(?ms).*\./include/includer .*`)
+       c.Check(manifest, check.Not(check.Matches), `(?ms).*exclude.*`)
        c.Check(s.log.String(), check.Matches, `(?ms).*not copying \\"exclude/excluder\\".*`)
        c.Check(s.log.String(), check.Matches, `(?ms).*not copying \\"nonexistent/collection\\".*`)
 }
@@ -359,7 +436,7 @@ func (s *copierSuite) writeFileInOutputDir(c *check.C, path, data string) {
 }
 
 // applyGlobsToFilesAndDirs uses the same glob-matching code as
-// applyGlobsToCollectionFS, so we don't need to test all of the same
+// applyGlobsToStaged, so we don't need to test all of the same
 // glob-matching behavior covered in TestApplyGlobsToCollectionFS.  We
 // do need to check that (a) the glob is actually being used to filter
 // out files, and (b) non-matching dirs still included if and only if
@@ -521,8 +598,8 @@ func (s *copierSuite) TestApplyGlobsToCollectionFS(c *check.C) {
                c.Logf("=== globs: %q", trial.globs)
                collfs, err := (&arvados.Collection{ManifestText: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo 0:0:bar 0:0:baz/quux 0:0:baz/parent1/item1\n"}).FileSystem(nil, nil)
                c.Assert(err, check.IsNil)
-               cp := copier{globs: trial.globs}
-               err = cp.applyGlobsToCollectionFS(collfs)
+               cp := copier{globs: trial.globs, staged: collfs}
+               err = cp.applyGlobsToStaged()
                if !c.Check(err, check.IsNil) {
                        continue
                }
index 7782a9c5a610b1490f5c180f2b0e4455f0c7b131..0fe8237bc7ad711557a8c90726db41abc8005b71 100644 (file)
@@ -40,7 +40,6 @@ import (
        "git.arvados.org/arvados.git/sdk/go/arvadosclient"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "git.arvados.org/arvados.git/sdk/go/keepclient"
-       "git.arvados.org/arvados.git/sdk/go/manifest"
        "golang.org/x/sys/unix"
 )
 
@@ -76,7 +75,6 @@ var ErrCancelled = errors.New("Cancelled")
 type IKeepClient interface {
        BlockWrite(context.Context, arvados.BlockWriteOptions) (arvados.BlockWriteResponse, error)
        ReadAt(locator string, p []byte, off int) (int, error)
-       ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error)
        LocalLocator(locator string) (string, error)
        SetStorageClasses(sc []string)
 }
index 93d615d3c757e4a8dc3ef5affaf628234ba1fa3d..708c9b9ad578b2a95191b08c882b22fb19538731 100644 (file)
@@ -36,7 +36,6 @@ import (
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/arvadosclient"
        "git.arvados.org/arvados.git/sdk/go/arvadostest"
-       "git.arvados.org/arvados.git/sdk/go/manifest"
 
        . "gopkg.in/check.v1"
 )
@@ -414,19 +413,6 @@ func (fw FileWrapper) Splice(*arvados.Subtree) error {
        return errors.New("not implemented")
 }
 
-func (client *KeepTestClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
-       if filename == hwImageID+".tar" {
-               rdr := ioutil.NopCloser(&bytes.Buffer{})
-               client.Called = true
-               return FileWrapper{rdr, 1321984}, nil
-       } else if filename == "/file1_in_main.txt" {
-               rdr := ioutil.NopCloser(strings.NewReader("foo"))
-               client.Called = true
-               return FileWrapper{rdr, 3}, nil
-       }
-       return nil, nil
-}
-
 type apiStubServer struct {
        server    *httptest.Server
        proxy     *httputil.ReverseProxy
@@ -556,10 +542,6 @@ type KeepErrorTestClient struct {
        KeepTestClient
 }
 
-func (*KeepErrorTestClient) ManifestFileReader(manifest.Manifest, string) (arvados.File, error) {
-       return nil, errors.New("KeepError")
-}
-
 func (*KeepErrorTestClient) BlockWrite(context.Context, arvados.BlockWriteOptions) (arvados.BlockWriteResponse, error) {
        return arvados.BlockWriteResponse{}, errors.New("KeepError")
 }
@@ -576,22 +558,6 @@ func (*KeepReadErrorTestClient) ReadAt(string, []byte, int) (int, error) {
        return 0, errors.New("KeepError")
 }
 
-type ErrorReader struct {
-       FileWrapper
-}
-
-func (ErrorReader) Read(p []byte) (n int, err error) {
-       return 0, errors.New("ErrorReader")
-}
-
-func (ErrorReader) Seek(int64, int) (int64, error) {
-       return 0, errors.New("ErrorReader")
-}
-
-func (*KeepReadErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
-       return ErrorReader{}, nil
-}
-
 func dockerLog(fd byte, msg string) []byte {
        by := []byte(msg)
        header := make([]byte, 8+len(by))
index 38c589f698118469f238cdb181832d48d4127f8e..5826105839b0aecb9b4fd276c792a5eddb177d42 100644 (file)
@@ -169,7 +169,7 @@ func (s *integrationSuite) TestRunTrivialContainerWithDocker(c *C) {
 func (s *integrationSuite) TestRunTrivialContainerWithSingularity(c *C) {
        s.engine = "singularity"
        s.testRunTrivialContainer(c)
-       c.Check(s.logFiles["crunch-run.txt"], Matches, `(?ms).*Using container runtime: singularity.* version 3\.\d+.*`)
+       c.Check(s.logFiles["crunch-run.txt"], Matches, `(?ms).*Using container runtime: singularity.* version [34]\.\d+.*`)
 }
 
 func (s *integrationSuite) TestRunTrivialContainerWithLocalKeepstore(c *C) {
index 2f9521c65dc225a071b4479cdef104bc3e4c4074..a99b8e6a7eaea73a8c328aed8fcde8526f1f6efd 100644 (file)
@@ -5,6 +5,7 @@
 package deduplicationreport
 
 import (
+       "bytes"
        "flag"
        "fmt"
        "io"
@@ -13,7 +14,7 @@ import (
        "git.arvados.org/arvados.git/lib/cmd"
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/arvadosclient"
-       "git.arvados.org/arvados.git/sdk/go/manifest"
+       "git.arvados.org/arvados.git/sdk/go/blockdigest"
 
        "github.com/dustin/go-humanize"
        "github.com/sirupsen/logrus"
@@ -91,10 +92,11 @@ Options:
 
 func blockList(collection arvados.Collection) (blocks map[string]int) {
        blocks = make(map[string]int)
-       m := manifest.Manifest{Text: collection.ManifestText}
-       blockChannel := m.BlockIterWithDuplicates()
-       for b := range blockChannel {
-               blocks[b.Digest.String()] = b.Size
+       for _, token := range bytes.Split([]byte(collection.ManifestText), []byte{' '}) {
+               if blockdigest.IsBlockLocator(string(token)) {
+                       loc, _ := blockdigest.ParseBlockLocator(string(token))
+                       blocks[loc.Digest.String()] = loc.Size
+               }
        }
        return
 }
index 9c5eeaccfd22451c523b906f35f96d9378c11d65..5909499bac54f3d9817260ea0d83142015f7c792 100644 (file)
@@ -203,6 +203,7 @@ func (inst *installCommand) RunCommand(prog string, args []string, stdin io.Read
                        "libxslt1-dev",
                        "libyaml-dev",
                        "linkchecker",
+                       "locales",
                        "lsof",
                        "make",
                        "net-tools",
@@ -222,6 +223,7 @@ func (inst *installCommand) RunCommand(prog string, args []string, stdin io.Read
                        "r-cran-xml",
                        "rsync",
                        "sudo",
+                       "unzip",
                        "uuid-dev",
                        "wget",
                        "xvfb",
index 5cf4892733194b30142fdd2a2fd43eeecb663eee..aaed7aa4c65d5030ea5f0c0f33812231ec80b2fb 100644 (file)
@@ -65,12 +65,12 @@ Installing on Red Hat, AlmaLinux, and Rocky Linux
 
 Arvados publishes packages for RHEL 8 and distributions based on it. Note that these packages depend on, and will automatically enable, the Python 3.9 module. You can install the Python SDK package on any of these distributions by running the following commands::
 
-  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<EOF
+  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<'EOF'
   [arvados]
   name=Arvados
-  baseurl=http://rpm.arvados.org/CentOS/\$releasever/os/\$basearch/
+  baseurl=http://rpm.arvados.org/RHEL/$releasever/os/$basearch/
   gpgcheck=1
-  gpgkey=http://rpm.arvados.org/CentOS/RPM-GPG-KEY-arvados
+  gpgkey=http://rpm.arvados.org/RHEL/RPM-GPG-KEY-arvados
   EOF
   sudo dnf install python3-arvados-cwl-runner
 
diff --git a/sdk/go/arvadostest/manifest.go b/sdk/go/arvadostest/manifest.go
new file mode 100644 (file)
index 0000000..f5939cd
--- /dev/null
@@ -0,0 +1,42 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvadostest
+
+import (
+       "bytes"
+       "fmt"
+       "math/rand"
+)
+
+func FakeManifest(dirCount, filesPerDir, blocksPerFile, interleaveChunk int) string {
+       const blksize = 1 << 26
+       mb := bytes.NewBuffer(make([]byte, 0, 40000000))
+       blkid := 0
+       for i := 0; i < dirCount; i++ {
+               fmt.Fprintf(mb, "./dir%d", i)
+               for j := 0; j < filesPerDir; j++ {
+                       for k := 0; k < blocksPerFile; k++ {
+                               blkid++
+                               fmt.Fprintf(mb, " %032x+%d+A%040x@%08x", blkid, blksize, blkid, blkid)
+                       }
+               }
+               for j := 0; j < filesPerDir; j++ {
+                       if interleaveChunk == 0 {
+                               fmt.Fprintf(mb, " %d:%d:dir%d/file%d", (filesPerDir-j-1)*blocksPerFile*blksize, blocksPerFile*blksize, j, j)
+                               continue
+                       }
+                       for todo := int64(blocksPerFile) * int64(blksize); todo > 0; todo -= int64(interleaveChunk) {
+                               size := int64(interleaveChunk)
+                               if size > todo {
+                                       size = todo
+                               }
+                               offset := rand.Int63n(int64(blocksPerFile)*int64(blksize)*int64(filesPerDir) - size)
+                               fmt.Fprintf(mb, " %d:%d:dir%d/file%d", offset, size, j, j)
+                       }
+               }
+               mb.Write([]byte{'\n'})
+       }
+       return mb.String()
+}
index 31a26671226a0a8ba3452e9e61a41a263bbd3429..2289bbef30612fbf5005c30d6612f3cb65836264 100644 (file)
@@ -17,8 +17,8 @@ import (
        "time"
 
        "gopkg.in/check.v1"
-       "gopkg.in/square/go-jose.v2"
-       "gopkg.in/square/go-jose.v2/jwt"
+       "gopkg.in/go-jose/go-jose.v2"
+       "gopkg.in/go-jose/go-jose.v2/jwt"
 )
 
 type OIDCProvider struct {
index ecb09964ecc50585a3c213a8b3cb1f8642fb5050..57593bea9c3ba4a644b49a3a621ac286baff97ff 100644 (file)
@@ -65,29 +65,24 @@ func IsBlockLocator(s string) bool {
        return LocatorPattern.MatchString(s)
 }
 
-func ParseBlockLocator(s string) (b BlockLocator, err error) {
+func ParseBlockLocator(s string) (BlockLocator, error) {
        if !LocatorPattern.MatchString(s) {
-               err = fmt.Errorf("String \"%s\" does not match BlockLocator pattern "+
-                       "\"%s\".",
-                       s,
-                       LocatorPattern.String())
-       } else {
-               tokens := strings.Split(s, "+")
-               var blockSize int64
-               var blockDigest BlockDigest
-               // We expect both of the following to succeed since LocatorPattern
-               // restricts the strings appropriately.
-               blockDigest, err = FromString(tokens[0])
-               if err != nil {
-                       return
-               }
-               blockSize, err = strconv.ParseInt(tokens[1], 10, 0)
-               if err != nil {
-                       return
-               }
-               b.Digest = blockDigest
-               b.Size = int(blockSize)
-               b.Hints = tokens[2:]
+               return BlockLocator{}, fmt.Errorf("String %q does not match block locator pattern %q.", s, LocatorPattern.String())
        }
-       return
+       tokens := strings.Split(s, "+")
+       // We expect both of the following to succeed since
+       // LocatorPattern restricts the strings appropriately.
+       blockDigest, err := FromString(tokens[0])
+       if err != nil {
+               return BlockLocator{}, err
+       }
+       blockSize, err := strconv.ParseInt(tokens[1], 10, 0)
+       if err != nil {
+               return BlockLocator{}, err
+       }
+       return BlockLocator{
+               Digest: blockDigest,
+               Size:   int(blockSize),
+               Hints:  tokens[2:],
+       }, nil
 }
index 8e4bb93bfa1f8eca6c37090cb930d636231e27d2..580e51461bb19ec1595a7d0c11d479556b7cda3a 100644 (file)
@@ -9,7 +9,6 @@ import (
        "os"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
-       "git.arvados.org/arvados.git/sdk/go/manifest"
 )
 
 // ErrNoManifest indicates the given collection has no manifest
@@ -31,11 +30,3 @@ func (kc *KeepClient) CollectionFileReader(collection map[string]interface{}, fi
        }
        return fs.OpenFile(filename, os.O_RDONLY, 0)
 }
-
-func (kc *KeepClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
-       fs, err := (&arvados.Collection{ManifestText: m.Text}).FileSystem(nil, kc)
-       if err != nil {
-               return nil, err
-       }
-       return fs.OpenFile(filename, os.O_RDONLY, 0)
-}
diff --git a/sdk/go/manifest/manifest.go b/sdk/go/manifest/manifest.go
deleted file mode 100644 (file)
index a597003..0000000
+++ /dev/null
@@ -1,559 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: Apache-2.0
-
-/* Deals with parsing Manifest Text. */
-
-// Inspired by the Manifest class in arvados/sdk/ruby/lib/arvados/keep.rb
-
-package manifest
-
-import (
-       "errors"
-       "fmt"
-       "path"
-       "regexp"
-       "sort"
-       "strconv"
-       "strings"
-
-       "git.arvados.org/arvados.git/sdk/go/blockdigest"
-)
-
-var ErrInvalidToken = errors.New("Invalid token")
-
-type Manifest struct {
-       Text string
-       Err  error
-}
-
-type BlockLocator struct {
-       Digest blockdigest.BlockDigest
-       Size   int
-       Hints  []string
-}
-
-// FileSegment is a portion of a file that is contained within a
-// single block.
-type FileSegment struct {
-       Locator string
-       // Offset (within this block) of this data segment
-       Offset int
-       Len    int
-}
-
-// FileStreamSegment is a portion of a file described as a segment of a stream.
-type FileStreamSegment struct {
-       SegPos uint64
-       SegLen uint64
-       Name   string
-}
-
-// ManifestStream represents a single line from a manifest.
-type ManifestStream struct {
-       StreamName         string
-       Blocks             []string
-       blockOffsets       []uint64
-       FileStreamSegments []FileStreamSegment
-       Err                error
-}
-
-// Array of segments referencing file content
-type segmentedFile []FileSegment
-
-// Map of files to list of file segments referencing file content
-type segmentedStream map[string]segmentedFile
-
-// Map of streams
-type segmentedManifest map[string]segmentedStream
-
-var escapeSeq = regexp.MustCompile(`\\([0-9]{3}|\\)`)
-
-func unescapeSeq(seq string) string {
-       if seq == `\\` {
-               return `\`
-       }
-       i, err := strconv.ParseUint(seq[1:], 8, 8)
-       if err != nil {
-               // Invalid escape sequence: can't unescape.
-               return seq
-       }
-       return string([]byte{byte(i)})
-}
-
-func EscapeName(s string) string {
-       raw := []byte(s)
-       escaped := make([]byte, 0, len(s))
-       for _, c := range raw {
-               if c <= 32 {
-                       oct := fmt.Sprintf("\\%03o", c)
-                       escaped = append(escaped, []byte(oct)...)
-               } else {
-                       escaped = append(escaped, c)
-               }
-       }
-       return string(escaped)
-}
-
-func UnescapeName(s string) string {
-       return escapeSeq.ReplaceAllStringFunc(s, unescapeSeq)
-}
-
-func ParseBlockLocator(s string) (b BlockLocator, err error) {
-       if !blockdigest.LocatorPattern.MatchString(s) {
-               err = fmt.Errorf("String \"%s\" does not match BlockLocator pattern "+
-                       "\"%s\".",
-                       s,
-                       blockdigest.LocatorPattern.String())
-       } else {
-               tokens := strings.Split(s, "+")
-               var blockSize int64
-               var blockDigest blockdigest.BlockDigest
-               // We expect both of the following to succeed since LocatorPattern
-               // restricts the strings appropriately.
-               blockDigest, err = blockdigest.FromString(tokens[0])
-               if err != nil {
-                       return
-               }
-               blockSize, err = strconv.ParseInt(tokens[1], 10, 0)
-               if err != nil {
-                       return
-               }
-               b.Digest = blockDigest
-               b.Size = int(blockSize)
-               b.Hints = tokens[2:]
-       }
-       return
-}
-
-func parseFileStreamSegment(tok string) (ft FileStreamSegment, err error) {
-       parts := strings.SplitN(tok, ":", 3)
-       if len(parts) != 3 {
-               err = ErrInvalidToken
-               return
-       }
-       ft.SegPos, err = strconv.ParseUint(parts[0], 10, 64)
-       if err != nil {
-               return
-       }
-       ft.SegLen, err = strconv.ParseUint(parts[1], 10, 64)
-       if err != nil {
-               return
-       }
-       ft.Name = UnescapeName(parts[2])
-       return
-}
-
-func (s *ManifestStream) FileSegmentIterByName(filepath string) <-chan *FileSegment {
-       ch := make(chan *FileSegment, 64)
-       go func() {
-               s.sendFileSegmentIterByName(filepath, ch)
-               close(ch)
-       }()
-       return ch
-}
-
-func firstBlock(offsets []uint64, rangeStart uint64) int {
-       // rangeStart/blockStart is the inclusive lower bound
-       // rangeEnd/blockEnd is the exclusive upper bound
-
-       hi := len(offsets) - 1
-       var lo int
-       i := ((hi + lo) / 2)
-       blockStart := offsets[i]
-       blockEnd := offsets[i+1]
-
-       // perform a binary search for the first block
-       // assumes that all of the blocks are contiguous, so rangeStart is guaranteed
-       // to either fall into the range of a block or be outside the block range entirely
-       for !(rangeStart >= blockStart && rangeStart < blockEnd) {
-               if lo == i {
-                       // must be out of range, fail
-                       return -1
-               }
-               if rangeStart > blockStart {
-                       lo = i
-               } else {
-                       hi = i
-               }
-               i = ((hi + lo) / 2)
-               blockStart = offsets[i]
-               blockEnd = offsets[i+1]
-       }
-       return i
-}
-
-func (s *ManifestStream) sendFileSegmentIterByName(filepath string, ch chan<- *FileSegment) {
-       // This is what streamName+"/"+fileName will look like:
-       target := fixStreamName(filepath)
-       for _, fTok := range s.FileStreamSegments {
-               wantPos := fTok.SegPos
-               wantLen := fTok.SegLen
-               name := fTok.Name
-
-               if s.StreamName+"/"+name != target {
-                       continue
-               }
-               if wantLen == 0 {
-                       ch <- &FileSegment{Locator: "d41d8cd98f00b204e9800998ecf8427e+0", Offset: 0, Len: 0}
-                       continue
-               }
-
-               // Binary search to determine first block in the stream
-               i := firstBlock(s.blockOffsets, wantPos)
-               if i == -1 {
-                       // Shouldn't happen, file segments are checked in parseManifestStream
-                       panic(fmt.Sprintf("File segment %v extends past end of stream", fTok))
-               }
-               for ; i < len(s.Blocks); i++ {
-                       blockPos := s.blockOffsets[i]
-                       blockEnd := s.blockOffsets[i+1]
-                       if blockEnd <= wantPos {
-                               // Shouldn't happen, FirstBlock() should start
-                               // us on the right block, so if this triggers
-                               // that means there is a bug.
-                               panic(fmt.Sprintf("Block end %v comes before start of file segment %v", blockEnd, wantPos))
-                       }
-                       if blockPos >= wantPos+wantLen {
-                               // current block comes after current file span
-                               break
-                       }
-
-                       fseg := FileSegment{
-                               Locator: s.Blocks[i],
-                               Offset:  0,
-                               Len:     int(blockEnd - blockPos),
-                       }
-                       if blockPos < wantPos {
-                               fseg.Offset = int(wantPos - blockPos)
-                               fseg.Len -= fseg.Offset
-                       }
-                       if blockEnd > wantPos+wantLen {
-                               fseg.Len = int(wantPos+wantLen-blockPos) - fseg.Offset
-                       }
-                       ch <- &fseg
-               }
-       }
-}
-
-func parseManifestStream(s string) (m ManifestStream) {
-       tokens := strings.Split(s, " ")
-
-       m.StreamName = UnescapeName(tokens[0])
-       if m.StreamName != "." && !strings.HasPrefix(m.StreamName, "./") {
-               m.Err = fmt.Errorf("Invalid stream name: %s", m.StreamName)
-               return
-       }
-
-       tokens = tokens[1:]
-       var i int
-       for i = 0; i < len(tokens); i++ {
-               if !blockdigest.IsBlockLocator(tokens[i]) {
-                       break
-               }
-       }
-       m.Blocks = tokens[:i]
-       fileTokens := tokens[i:]
-
-       if len(m.Blocks) == 0 {
-               m.Err = fmt.Errorf("No block locators found")
-               return
-       }
-
-       m.blockOffsets = make([]uint64, len(m.Blocks)+1)
-       var streamoffset uint64
-       for i, b := range m.Blocks {
-               bl, err := ParseBlockLocator(b)
-               if err != nil {
-                       m.Err = err
-                       return
-               }
-               m.blockOffsets[i] = streamoffset
-               streamoffset += uint64(bl.Size)
-       }
-       m.blockOffsets[len(m.Blocks)] = streamoffset
-
-       if len(fileTokens) == 0 {
-               m.Err = fmt.Errorf("No file tokens found")
-               return
-       }
-
-       for _, ft := range fileTokens {
-               pft, err := parseFileStreamSegment(ft)
-               if err != nil {
-                       m.Err = fmt.Errorf("Invalid file token: %s", ft)
-                       break
-               }
-               if pft.SegPos+pft.SegLen > streamoffset {
-                       m.Err = fmt.Errorf("File segment %s extends past end of stream %d", ft, streamoffset)
-                       break
-               }
-               m.FileStreamSegments = append(m.FileStreamSegments, pft)
-       }
-
-       return
-}
-
-func fixStreamName(sn string) string {
-       sn = path.Clean(sn)
-       if strings.HasPrefix(sn, "/") {
-               sn = "." + sn
-       } else if sn != "." {
-               sn = "./" + sn
-       }
-       return sn
-}
-
-func splitPath(srcpath string) (streamname, filename string) {
-       pathIdx := strings.LastIndex(srcpath, "/")
-       if pathIdx >= 0 {
-               streamname = srcpath[0:pathIdx]
-               filename = srcpath[pathIdx+1:]
-       } else {
-               streamname = srcpath
-               filename = ""
-       }
-       return
-}
-
-func (m *Manifest) segment() (*segmentedManifest, error) {
-       files := make(segmentedManifest)
-
-       for stream := range m.StreamIter() {
-               if stream.Err != nil {
-                       // Stream has an error
-                       return nil, stream.Err
-               }
-               currentStreamfiles := make(map[string]bool)
-               for _, f := range stream.FileStreamSegments {
-                       sn := stream.StreamName
-                       if strings.HasSuffix(sn, "/") {
-                               sn = sn[0 : len(sn)-1]
-                       }
-                       path := sn + "/" + f.Name
-                       streamname, filename := splitPath(path)
-                       if files[streamname] == nil {
-                               files[streamname] = make(segmentedStream)
-                       }
-                       if !currentStreamfiles[path] {
-                               segs := files[streamname][filename]
-                               for seg := range stream.FileSegmentIterByName(path) {
-                                       if seg.Len > 0 {
-                                               segs = append(segs, *seg)
-                                       }
-                               }
-                               files[streamname][filename] = segs
-                               currentStreamfiles[path] = true
-                       }
-               }
-       }
-
-       return &files, nil
-}
-
-func (stream segmentedStream) normalizedText(name string) string {
-       var sortedfiles []string
-       for k := range stream {
-               sortedfiles = append(sortedfiles, k)
-       }
-       sort.Strings(sortedfiles)
-
-       streamTokens := []string{EscapeName(name)}
-
-       blocks := make(map[blockdigest.BlockDigest]int64)
-       var streamoffset int64
-
-       // Go through each file and add each referenced block exactly once.
-       for _, streamfile := range sortedfiles {
-               for _, segment := range stream[streamfile] {
-                       b, _ := ParseBlockLocator(segment.Locator)
-                       if _, ok := blocks[b.Digest]; !ok {
-                               streamTokens = append(streamTokens, segment.Locator)
-                               blocks[b.Digest] = streamoffset
-                               streamoffset += int64(b.Size)
-                       }
-               }
-       }
-
-       if len(streamTokens) == 1 {
-               streamTokens = append(streamTokens, "d41d8cd98f00b204e9800998ecf8427e+0")
-       }
-
-       for _, streamfile := range sortedfiles {
-               // Add in file segments
-               spanStart := int64(-1)
-               spanEnd := int64(0)
-               fout := EscapeName(streamfile)
-               for _, segment := range stream[streamfile] {
-                       // Collapse adjacent segments
-                       b, _ := ParseBlockLocator(segment.Locator)
-                       streamoffset = blocks[b.Digest] + int64(segment.Offset)
-                       if spanStart == -1 {
-                               spanStart = streamoffset
-                               spanEnd = streamoffset + int64(segment.Len)
-                       } else {
-                               if streamoffset == spanEnd {
-                                       spanEnd += int64(segment.Len)
-                               } else {
-                                       streamTokens = append(streamTokens, fmt.Sprintf("%d:%d:%s", spanStart, spanEnd-spanStart, fout))
-                                       spanStart = streamoffset
-                                       spanEnd = streamoffset + int64(segment.Len)
-                               }
-                       }
-               }
-
-               if spanStart != -1 {
-                       streamTokens = append(streamTokens, fmt.Sprintf("%d:%d:%s", spanStart, spanEnd-spanStart, fout))
-               }
-
-               if len(stream[streamfile]) == 0 {
-                       streamTokens = append(streamTokens, fmt.Sprintf("0:0:%s", fout))
-               }
-       }
-
-       return strings.Join(streamTokens, " ") + "\n"
-}
-
-func (m segmentedManifest) manifestTextForPath(srcpath, relocate string) string {
-       srcpath = fixStreamName(srcpath)
-
-       var suffix string
-       if strings.HasSuffix(relocate, "/") {
-               suffix = "/"
-       }
-       relocate = fixStreamName(relocate) + suffix
-
-       streamname, filename := splitPath(srcpath)
-
-       if stream, ok := m[streamname]; ok {
-               // check if it refers to a single file in a stream
-               filesegs, okfile := stream[filename]
-               if okfile {
-                       newstream := make(segmentedStream)
-                       relocateStream, relocateFilename := splitPath(relocate)
-                       if relocateFilename == "" {
-                               relocateFilename = filename
-                       }
-                       newstream[relocateFilename] = filesegs
-                       return newstream.normalizedText(relocateStream)
-               }
-       }
-
-       // Going to extract multiple streams
-       prefix := srcpath + "/"
-
-       if strings.HasSuffix(relocate, "/") {
-               relocate = relocate[0 : len(relocate)-1]
-       }
-
-       var sortedstreams []string
-       for k := range m {
-               sortedstreams = append(sortedstreams, k)
-       }
-       sort.Strings(sortedstreams)
-
-       manifest := ""
-       for _, k := range sortedstreams {
-               if strings.HasPrefix(k, prefix) || k == srcpath {
-                       manifest += m[k].normalizedText(relocate + k[len(srcpath):])
-               }
-       }
-       return manifest
-}
-
-// Extract extracts some or all of the manifest and returns the extracted
-// portion as a normalized manifest.  This is a swiss army knife function that
-// can be several ways:
-//
-// If 'srcpath' and 'relocate' are '.' it simply returns an equivalent manifest
-// in normalized form.
-//
-//     Extract(".", ".")  // return entire normalized manfest text
-//
-// If 'srcpath' points to a single file, it will return manifest text for just that file.
-// The value of "relocate" is can be used to rename the file or set the file stream.
-//
-//     Extract("./foo", ".")          // extract file "foo" and put it in stream "."
-//     Extract("./foo", "./bar")      // extract file "foo", rename it to "bar" in stream "."
-//     Extract("./foo", "./bar/")     // extract file "foo", rename it to "./bar/foo"
-//     Extract("./foo", "./bar/baz")  // extract file "foo", rename it to "./bar/baz")
-//
-// Otherwise it will return the manifest text for all streams with the prefix in "srcpath" and place
-// them under the path in "relocate".
-//
-//     Extract("./stream", ".")      // extract "./stream" to "." and "./stream/subdir" to "./subdir")
-//     Extract("./stream", "./bar")  // extract "./stream" to "./bar" and "./stream/subdir" to "./bar/subdir")
-func (m Manifest) Extract(srcpath, relocate string) (ret Manifest) {
-       segmented, err := m.segment()
-       if err != nil {
-               ret.Err = err
-               return
-       }
-       ret.Text = segmented.manifestTextForPath(srcpath, relocate)
-       return
-}
-
-func (m *Manifest) StreamIter() <-chan ManifestStream {
-       ch := make(chan ManifestStream)
-       go func(input string) {
-               // This slice holds the current line and the remainder of the
-               // manifest.  We parse one line at a time, to save effort if we
-               // only need the first few lines.
-               lines := []string{"", input}
-               for {
-                       lines = strings.SplitN(lines[1], "\n", 2)
-                       if len(lines[0]) > 0 {
-                               // Only parse non-blank lines
-                               ch <- parseManifestStream(lines[0])
-                       }
-                       if len(lines) == 1 {
-                               break
-                       }
-               }
-               close(ch)
-       }(m.Text)
-       return ch
-}
-
-func (m *Manifest) FileSegmentIterByName(filepath string) <-chan *FileSegment {
-       ch := make(chan *FileSegment, 64)
-       filepath = fixStreamName(filepath)
-       go func() {
-               for stream := range m.StreamIter() {
-                       if !strings.HasPrefix(filepath, stream.StreamName+"/") {
-                               continue
-                       }
-                       stream.sendFileSegmentIterByName(filepath, ch)
-               }
-               close(ch)
-       }()
-       return ch
-}
-
-// BlockIterWithDuplicates iterates over the block locators of a manifest.
-//
-// Blocks may appear multiple times within the same manifest if they
-// are used by multiple files. In that case this Iterator will output
-// the same block multiple times.
-//
-// In order to detect parse errors, caller must check m.Err after the returned channel closes.
-func (m *Manifest) BlockIterWithDuplicates() <-chan blockdigest.BlockLocator {
-       blockChannel := make(chan blockdigest.BlockLocator)
-       go func(streamChannel <-chan ManifestStream) {
-               for ms := range streamChannel {
-                       if ms.Err != nil {
-                               m.Err = ms.Err
-                               continue
-                       }
-                       for _, block := range ms.Blocks {
-                               if b, err := blockdigest.ParseBlockLocator(block); err == nil {
-                                       blockChannel <- b
-                               } else {
-                                       m.Err = err
-                               }
-                       }
-               }
-               close(blockChannel)
-       }(m.StreamIter())
-       return blockChannel
-}
diff --git a/sdk/go/manifest/manifest_test.go b/sdk/go/manifest/manifest_test.go
deleted file mode 100644 (file)
index 090ead9..0000000
+++ /dev/null
@@ -1,375 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: Apache-2.0
-
-package manifest
-
-import (
-       "fmt"
-       "git.arvados.org/arvados.git/sdk/go/arvadostest"
-       "git.arvados.org/arvados.git/sdk/go/blockdigest"
-       "io/ioutil"
-       "reflect"
-       "regexp"
-       "runtime"
-       "testing"
-)
-
-func getStackTrace() string {
-       buf := make([]byte, 1000)
-       bytesWritten := runtime.Stack(buf, false)
-       return "Stack Trace:\n" + string(buf[:bytesWritten])
-}
-
-func expectFromChannel(t *testing.T, c <-chan string, expected string) {
-       actual, ok := <-c
-       if !ok {
-               t.Fatalf("Expected to receive %s but channel was closed. %s",
-                       expected,
-                       getStackTrace())
-       }
-       if actual != expected {
-               t.Fatalf("Expected %s but got %s instead. %s",
-                       expected,
-                       actual,
-                       getStackTrace())
-       }
-}
-
-func expectChannelClosed(t *testing.T, c <-chan interface{}) {
-       received, ok := <-c
-       if ok {
-               t.Fatalf("Expected channel to be closed, but received %v instead. %s",
-                       received,
-                       getStackTrace())
-       }
-}
-
-func expectEqual(t *testing.T, actual interface{}, expected interface{}) {
-       if actual != expected {
-               t.Fatalf("Expected %v but received %v instead. %s",
-                       expected,
-                       actual,
-                       getStackTrace())
-       }
-}
-
-func expectStringSlicesEqual(t *testing.T, actual []string, expected []string) {
-       if len(actual) != len(expected) {
-               t.Fatalf("Expected %v (length %d), but received %v (length %d) instead. %s", expected, len(expected), actual, len(actual), getStackTrace())
-       }
-       for i := range actual {
-               if actual[i] != expected[i] {
-                       t.Fatalf("Expected %v but received %v instead (first disagreement at position %d). %s", expected, actual, i, getStackTrace())
-               }
-       }
-}
-
-func expectFileStreamSegmentsEqual(t *testing.T, actual []FileStreamSegment, expected []FileStreamSegment) {
-       if !reflect.DeepEqual(actual, expected) {
-               t.Fatalf("Expected %v but received %v instead. %s", expected, actual, getStackTrace())
-       }
-}
-
-func expectManifestStream(t *testing.T, actual ManifestStream, expected ManifestStream) {
-       expectEqual(t, actual.StreamName, expected.StreamName)
-       expectStringSlicesEqual(t, actual.Blocks, expected.Blocks)
-       expectFileStreamSegmentsEqual(t, actual.FileStreamSegments, expected.FileStreamSegments)
-}
-
-func expectBlockLocator(t *testing.T, actual blockdigest.BlockLocator, expected blockdigest.BlockLocator) {
-       expectEqual(t, actual.Digest, expected.Digest)
-       expectEqual(t, actual.Size, expected.Size)
-       expectStringSlicesEqual(t, actual.Hints, expected.Hints)
-}
-
-func TestParseManifestStreamSimple(t *testing.T) {
-       m := parseManifestStream(". 365f83f5f808896ec834c8b595288735+2310+K@qr1hi+Af0c9a66381f3b028677411926f0be1c6282fe67c@542b5ddf 0:2310:qr1hi-8i9sb-ienvmpve1a0vpoi.log.txt")
-       expectManifestStream(t, m, ManifestStream{StreamName: ".",
-               Blocks:             []string{"365f83f5f808896ec834c8b595288735+2310+K@qr1hi+Af0c9a66381f3b028677411926f0be1c6282fe67c@542b5ddf"},
-               FileStreamSegments: []FileStreamSegment{{0, 2310, "qr1hi-8i9sb-ienvmpve1a0vpoi.log.txt"}}})
-}
-
-func TestParseBlockLocatorSimple(t *testing.T) {
-       b, err := ParseBlockLocator("365f83f5f808896ec834c8b595288735+2310+K@qr1hi+Af0c9a66381f3b028677411926f0be1c6282fe67c@542b5ddf")
-       if err != nil {
-               t.Fatalf("Unexpected error parsing block locator: %v", err)
-       }
-       d, err := blockdigest.FromString("365f83f5f808896ec834c8b595288735")
-       if err != nil {
-               t.Fatalf("Unexpected error during FromString for block locator: %v", err)
-       }
-       expectBlockLocator(t, blockdigest.BlockLocator{b.Digest, b.Size, b.Hints},
-               blockdigest.BlockLocator{Digest: d,
-                       Size: 2310,
-                       Hints: []string{"K@qr1hi",
-                               "Af0c9a66381f3b028677411926f0be1c6282fe67c@542b5ddf"}})
-}
-
-func TestStreamIterShortManifestWithBlankStreams(t *testing.T) {
-       content, err := ioutil.ReadFile("testdata/short_manifest")
-       if err != nil {
-               t.Fatalf("Unexpected error reading manifest from file: %v", err)
-       }
-       manifest := Manifest{Text: string(content)}
-       streamIter := manifest.StreamIter()
-
-       firstStream := <-streamIter
-       expectManifestStream(t,
-               firstStream,
-               ManifestStream{StreamName: ".",
-                       Blocks:             []string{"b746e3d2104645f2f64cd3cc69dd895d+15693477+E2866e643690156651c03d876e638e674dcd79475@5441920c"},
-                       FileStreamSegments: []FileStreamSegment{{0, 15693477, "chr10_band0_s0_e3000000.fj"}}})
-
-       received, ok := <-streamIter
-       if ok {
-               t.Fatalf("Expected streamIter to be closed, but received %v instead.",
-                       received)
-       }
-}
-
-func TestBlockIterLongManifest(t *testing.T) {
-       content, err := ioutil.ReadFile("testdata/long_manifest")
-       if err != nil {
-               t.Fatalf("Unexpected error reading manifest from file: %v", err)
-       }
-       manifest := Manifest{Text: string(content)}
-       blockChannel := manifest.BlockIterWithDuplicates()
-
-       firstBlock := <-blockChannel
-       d, err := blockdigest.FromString("b746e3d2104645f2f64cd3cc69dd895d")
-       if err != nil {
-               t.Fatalf("Unexpected error during FromString for block: %v", err)
-       }
-       expectBlockLocator(t,
-               firstBlock,
-               blockdigest.BlockLocator{Digest: d,
-                       Size:  15693477,
-                       Hints: []string{"E2866e643690156651c03d876e638e674dcd79475@5441920c"}})
-       blocksRead := 1
-       var lastBlock blockdigest.BlockLocator
-       for lastBlock = range blockChannel {
-               blocksRead++
-       }
-       expectEqual(t, blocksRead, 853)
-
-       d, err = blockdigest.FromString("f9ce82f59e5908d2d70e18df9679b469")
-       if err != nil {
-               t.Fatalf("Unexpected error during FromString for block: %v", err)
-       }
-       expectBlockLocator(t,
-               lastBlock,
-               blockdigest.BlockLocator{Digest: d,
-                       Size:  31367794,
-                       Hints: []string{"E53f903684239bcc114f7bf8ff9bd6089f33058db@5441920c"}})
-}
-
-func TestUnescape(t *testing.T) {
-       for _, testCase := range [][]string{
-               {`\040`, ` `},
-               {`\009`, `\009`},
-               {`\\\040\\`, `\ \`},
-               {`\\040\`, `\040\`},
-       } {
-               in := testCase[0]
-               expect := testCase[1]
-               got := UnescapeName(in)
-               if expect != got {
-                       t.Errorf("For '%s' got '%s' instead of '%s'", in, got, expect)
-               }
-       }
-}
-
-type fsegtest struct {
-       mt   string        // manifest text
-       f    string        // filename
-       want []FileSegment // segments should be received on channel
-}
-
-func TestFileSegmentIterByName(t *testing.T) {
-       mt := arvadostest.PathologicalManifest
-       for _, testCase := range []fsegtest{
-               {mt: mt, f: "zzzz", want: nil},
-               // This case is too sensitive: it would be acceptable
-               // (even preferable) to return only one empty segment.
-               {mt: mt, f: "foo/zero", want: []FileSegment{{"d41d8cd98f00b204e9800998ecf8427e+0", 0, 0}, {"d41d8cd98f00b204e9800998ecf8427e+0", 0, 0}}},
-               {mt: mt, f: "zero@0", want: []FileSegment{{"d41d8cd98f00b204e9800998ecf8427e+0", 0, 0}}},
-               {mt: mt, f: "zero@1", want: []FileSegment{{"d41d8cd98f00b204e9800998ecf8427e+0", 0, 0}}},
-               {mt: mt, f: "zero@4", want: []FileSegment{{"d41d8cd98f00b204e9800998ecf8427e+0", 0, 0}}},
-               {mt: mt, f: "zero@9", want: []FileSegment{{"d41d8cd98f00b204e9800998ecf8427e+0", 0, 0}}},
-               {mt: mt, f: "f", want: []FileSegment{{"acbd18db4cc2f85cedef654fccc4a4d8+3", 0, 1}}},
-               {mt: mt, f: "ooba", want: []FileSegment{{"acbd18db4cc2f85cedef654fccc4a4d8+3", 1, 2}, {"37b51d194a7513e45b56f6524f2d51f2+3", 0, 2}}},
-               {mt: mt, f: "overlapReverse/o", want: []FileSegment{{"acbd18db4cc2f85cedef654fccc4a4d8+3", 2, 1}}},
-               {mt: mt, f: "overlapReverse/oo", want: []FileSegment{{"acbd18db4cc2f85cedef654fccc4a4d8+3", 1, 2}}},
-               {mt: mt, f: "overlapReverse/ofoo", want: []FileSegment{{"acbd18db4cc2f85cedef654fccc4a4d8+3", 2, 1}, {"acbd18db4cc2f85cedef654fccc4a4d8+3", 0, 3}}},
-               {mt: mt, f: "foo bar/baz", want: []FileSegment{{"acbd18db4cc2f85cedef654fccc4a4d8+3", 0, 3}}},
-               // This case is too sensitive: it would be better to
-               // omit the empty segment.
-               {mt: mt, f: "segmented/frob", want: []FileSegment{{"acbd18db4cc2f85cedef654fccc4a4d8+3", 0, 1}, {"37b51d194a7513e45b56f6524f2d51f2+3", 2, 1}, {"acbd18db4cc2f85cedef654fccc4a4d8+3", 1, 1}, {"d41d8cd98f00b204e9800998ecf8427e+0", 0, 0}, {"37b51d194a7513e45b56f6524f2d51f2+3", 0, 1}}},
-               {mt: mt, f: "segmented/oof", want: []FileSegment{{"acbd18db4cc2f85cedef654fccc4a4d8+3", 1, 2}, {"acbd18db4cc2f85cedef654fccc4a4d8+3", 0, 1}}},
-       } {
-               m := Manifest{Text: testCase.mt}
-               var got []FileSegment
-               for fs := range m.FileSegmentIterByName(testCase.f) {
-                       got = append(got, *fs)
-               }
-               if !reflect.DeepEqual(got, testCase.want) {
-                       t.Errorf("For %#v:\n got  %#v\n want %#v", testCase.f, got, testCase.want)
-               }
-       }
-}
-
-func TestBlockIterWithBadManifest(t *testing.T) {
-       testCases := [][]string{
-               {"badstream acbd18db4cc2f85cedef654fccc4a4d8+3 0:1:file1.txt", "Invalid stream name: badstream"},
-               {"/badstream acbd18db4cc2f85cedef654fccc4a4d8+3 0:1:file1.txt", "Invalid stream name: /badstream"},
-               {". acbd18db4cc2f85cedef654fccc4a4d8+3 file1.txt", "Invalid file token: file1.txt"},
-               {". acbd18db4cc2f85cedef654fccc4a4+3 0:1:file1.txt", "No block locators found"},
-               {". acbd18db4cc2f85cedef654fccc4a4d8 0:1:file1.txt", "No block locators found"},
-               {". acbd18db4cc2f85cedef654fccc4a4d8+3 0:1:file1.txt file2.txt 1:2:file3.txt", "Invalid file token: file2.txt"},
-               {". acbd18db4cc2f85cedef654fccc4a4d8+3 0:1:file1.txt. bcde18db4cc2f85cedef654fccc4a4d8+3 1:2:file3.txt", "Invalid file token: bcde18db4cc2f85cedef654fccc4a4d8.*"},
-               {". acbd18db4cc2f85cedef654fccc4a4d8+3 0:1:file1.txt\n. acbd18db4cc2f85cedef654fccc4a4d8+3 ::file2.txt\n", "Invalid file token: ::file2.txt"},
-               {". acbd18db4cc2f85cedef654fccc4a4d8+3 bcde18db4cc2f85cedef654fccc4a4d8+3\n", "No file tokens found"},
-               {". acbd18db4cc2f85cedef654fccc4a4d8+3 ", "Invalid file token"},
-               {". acbd18db4cc2f85cedef654fccc4a4d8+3", "No file tokens found"},
-               {". 0:1:file1.txt\n", "No block locators found"},
-               {".\n", "No block locators found"},
-       }
-
-       for _, testCase := range testCases {
-               manifest := Manifest{Text: string(testCase[0])}
-               blockChannel := manifest.BlockIterWithDuplicates()
-
-               for block := range blockChannel {
-                       _ = block
-               }
-
-               // completed reading from blockChannel; now check for errors
-               if manifest.Err == nil {
-                       t.Fatalf("Expected error")
-               }
-
-               matched, _ := regexp.MatchString(testCase[1], manifest.Err.Error())
-               if !matched {
-                       t.Fatalf("Expected error not found. Expected: %v; Found: %v", testCase[1], manifest.Err.Error())
-               }
-       }
-}
-
-func TestNormalizeManifest(t *testing.T) {
-       m1 := Manifest{Text: `. 5348b82a029fd9e971a811ce1f71360b+43 0:43:md5sum.txt
-. 085c37f02916da1cad16f93c54d899b7+41 0:41:md5sum.txt
-. 8b22da26f9f433dea0a10e5ec66d73ba+43 0:43:md5sum.txt
-`}
-       expectEqual(t, m1.Extract(".", ".").Text,
-               `. 5348b82a029fd9e971a811ce1f71360b+43 085c37f02916da1cad16f93c54d899b7+41 8b22da26f9f433dea0a10e5ec66d73ba+43 0:127:md5sum.txt
-`)
-
-       m2 := Manifest{Text: `. 204e43b8a1185621ca55a94839582e6f+67108864 b9677abbac956bd3e86b1deb28dfac03+67108864 fc15aff2a762b13f521baf042140acec+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:227212247:var-GS000016015-ASM.tsv.bz2
-`}
-       expectEqual(t, m2.Extract(".", ".").Text, m2.Text)
-
-       m3 := Manifest{Text: `. 5348b82a029fd9e971a811ce1f71360b+43 3:40:md5sum.txt
-. 085c37f02916da1cad16f93c54d899b7+41 0:41:md5sum.txt
-. 8b22da26f9f433dea0a10e5ec66d73ba+43 0:43:md5sum.txt
-`}
-       expectEqual(t, m3.Extract(".", ".").Text, `. 5348b82a029fd9e971a811ce1f71360b+43 085c37f02916da1cad16f93c54d899b7+41 8b22da26f9f433dea0a10e5ec66d73ba+43 3:124:md5sum.txt
-`)
-       expectEqual(t, m3.Extract("/md5sum.txt", "/wiggle.txt").Text, `. 5348b82a029fd9e971a811ce1f71360b+43 085c37f02916da1cad16f93c54d899b7+41 8b22da26f9f433dea0a10e5ec66d73ba+43 3:124:wiggle.txt
-`)
-
-       m4 := Manifest{Text: `. 204e43b8a1185621ca55a94839582e6f+67108864 0:3:foo/bar
-./zzz 204e43b8a1185621ca55a94839582e6f+67108864 0:999:zzz
-./foo 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar
-`}
-
-       expectEqual(t, m4.Extract(".", ".").Text,
-               `./foo 204e43b8a1185621ca55a94839582e6f+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar 67108864:3:bar
-./zzz 204e43b8a1185621ca55a94839582e6f+67108864 0:999:zzz
-`)
-
-       expectEqual(t, m4.Extract("./foo", ".").Text, ". 204e43b8a1185621ca55a94839582e6f+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar 67108864:3:bar\n")
-       expectEqual(t, m4.Extract("./foo", "./baz").Text, "./baz 204e43b8a1185621ca55a94839582e6f+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar 67108864:3:bar\n")
-       expectEqual(t, m4.Extract("./foo/bar", ".").Text, ". 204e43b8a1185621ca55a94839582e6f+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar 67108864:3:bar\n")
-       expectEqual(t, m4.Extract("./foo/bar", "./baz").Text, ". 204e43b8a1185621ca55a94839582e6f+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:baz 67108864:3:baz\n")
-       expectEqual(t, m4.Extract("./foo/bar", "./quux/").Text, "./quux 204e43b8a1185621ca55a94839582e6f+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar 67108864:3:bar\n")
-       expectEqual(t, m4.Extract("./foo/bar", "./quux/baz").Text, "./quux 204e43b8a1185621ca55a94839582e6f+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:baz 67108864:3:baz\n")
-       expectEqual(t, m4.Extract(".", ".").Text, `./foo 204e43b8a1185621ca55a94839582e6f+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar 67108864:3:bar
-./zzz 204e43b8a1185621ca55a94839582e6f+67108864 0:999:zzz
-`)
-       expectEqual(t, m4.Extract(".", "./zip").Text, `./zip/foo 204e43b8a1185621ca55a94839582e6f+67108864 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar 67108864:3:bar
-./zip/zzz 204e43b8a1185621ca55a94839582e6f+67108864 0:999:zzz
-`)
-
-       expectEqual(t, m4.Extract("foo/.//bar/../../zzz/", "/waz/").Text, `./waz 204e43b8a1185621ca55a94839582e6f+67108864 0:999:zzz
-`)
-
-       m5 := Manifest{Text: `. 204e43b8a1185621ca55a94839582e6f+67108864 0:3:foo/bar
-./zzz 204e43b8a1185621ca55a94839582e6f+67108864 0:999:zzz
-./foo 204e43b8a1185621ca55a94839582e6f+67108864 3:3:bar
-`}
-       expectEqual(t, m5.Extract(".", ".").Text,
-               `./foo 204e43b8a1185621ca55a94839582e6f+67108864 0:6:bar
-./zzz 204e43b8a1185621ca55a94839582e6f+67108864 0:999:zzz
-`)
-
-       m8 := Manifest{Text: `./a\040b\040c 59ca0efa9f5633cb0371bbc0355478d8+13 0:13:hello\040world.txt
-`}
-       expectEqual(t, m8.Extract(".", ".").Text, m8.Text)
-
-       m9 := Manifest{Text: ". acbd18db4cc2f85cedef654fccc4a4d8+40 0:10:one 20:10:two 10:10:one 30:10:two\n"}
-       expectEqual(t, m9.Extract("", "").Text, ". acbd18db4cc2f85cedef654fccc4a4d8+40 0:20:one 20:20:two\n")
-
-       m10 := Manifest{Text: ". acbd18db4cc2f85cedef654fccc4a4d8+40 0:10:one 20:10:two 10:10:one 30:10:two\n"}
-       expectEqual(t, m10.Extract("./two", "./three").Text, ". acbd18db4cc2f85cedef654fccc4a4d8+40 20:20:three\n")
-
-       m11 := Manifest{Text: arvadostest.PathologicalManifest}
-       expectEqual(t, m11.Extract(".", ".").Text, `. acbd18db4cc2f85cedef654fccc4a4d8+3 37b51d194a7513e45b56f6524f2d51f2+3 73feffa4b7f6bb68e44cf984c85f6e88+3+Z+K@xyzzy 0:1:f 1:4:ooba 5:1:r 5:4:rbaz 0:0:zero@0 0:0:zero@1 0:0:zero@4 0:0:zero@9
-./foo acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo 0:3:foo 0:0:zero
-./foo\040bar acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:baz 0:3:baz\040waz
-./overlapReverse acbd18db4cc2f85cedef654fccc4a4d8+3 2:1:o 2:1:ofoo 0:3:ofoo 1:2:oo
-./segmented acbd18db4cc2f85cedef654fccc4a4d8+3 37b51d194a7513e45b56f6524f2d51f2+3 0:1:frob 5:1:frob 1:1:frob 3:1:frob 1:2:oof 0:1:oof
-`)
-
-       m12 := Manifest{Text: `./foo 204e43b8a1185621ca55a94839582e6f+67108864 0:3:bar
-./zzz 204e43b8a1185621ca55a94839582e6f+67108864 0:999:zzz
-./foo/baz 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar
-`}
-
-       expectEqual(t, m12.Extract("./foo", ".").Text, `. 204e43b8a1185621ca55a94839582e6f+67108864 0:3:bar
-./baz 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar
-`)
-       expectEqual(t, m12.Extract("./foo", "./blub").Text, `./blub 204e43b8a1185621ca55a94839582e6f+67108864 0:3:bar
-./blub/baz 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar
-`)
-       expectEqual(t, m12.Extract("./foo", "./blub/").Text, `./blub 204e43b8a1185621ca55a94839582e6f+67108864 0:3:bar
-./blub/baz 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar
-`)
-       expectEqual(t, m12.Extract("./foo/", "./blub/").Text, `./blub 204e43b8a1185621ca55a94839582e6f+67108864 0:3:bar
-./blub/baz 323d2a3ce20370c4ca1d3462a344f8fd+25885655 0:3:bar
-`)
-
-       m13 := Manifest{Text: `foo 204e43b8a1185621ca55a94839582e6f+67108864 0:3:bar
-`}
-
-       expectEqual(t, m13.Extract(".", ".").Text, ``)
-       expectEqual(t, m13.Extract(".", ".").Err.Error(), "Invalid stream name: foo")
-
-       m14 := Manifest{Text: `./foo 204e43b8a1185621ca55a94839582e6f+67108864 67108863:3:bar
-`}
-
-       expectEqual(t, m14.Extract(".", ".").Text, ``)
-       expectEqual(t, m14.Extract(".", ".").Err.Error(), "File segment 67108863:3:bar extends past end of stream 67108864")
-
-       m15 := Manifest{Text: `./foo 204e43b8a1185621ca55a94839582e6f+67108864 0:3bar
-`}
-
-       expectEqual(t, m15.Extract(".", ".").Text, ``)
-       expectEqual(t, m15.Extract(".", ".").Err.Error(), "Invalid file token: 0:3bar")
-}
-
-func TestFirstBlock(t *testing.T) {
-       fmt.Println("ZZZ")
-       expectEqual(t, firstBlock([]uint64{1, 2, 3, 4}, 3), 2)
-       expectEqual(t, firstBlock([]uint64{1, 2, 3, 4, 5, 6}, 4), 3)
-}
diff --git a/sdk/go/manifest/testdata/long_manifest b/sdk/go/manifest/testdata/long_manifest
deleted file mode 100644 (file)
index a7949e6..0000000
+++ /dev/null
@@ -1 +0,0 @@
-. b746e3d2104645f2f64cd3cc69dd895d+15693477+E2866e643690156651c03d876e638e674dcd79475@5441920c 109cd35b4d3f83266b63fb46c6943454+6770629+Ed0c0561b669237162996223b813b811d248ff9b0@5441920c 1455890e7b56831edff40738856e4194+15962669+Ec298b770d14205b5185d0e2b016ddd940c745446@5441920c 8c87f1c69c6f302c8c05e7d0e740d233+16342794+Ec432f4c24e63b840c1f12976b9edf396d70b8f67@5441920c 451cfce8c67bf92b67b5c6190d45d4f5+5067634+E406821d6ceb1d16ec638e66b7603c69f3482d895@5441920c f963d174978dc966910be6240e8602c7+4264756+E00241238e18635fdb583dd0c6d6561b672996467@5441920c 33be2d8cdd100eec6e842f644556d031+16665404+E6c773004b8296523014b9d23ed066ec72387485e@5441920c 6db13c2df6342b52d72df469c065b675+13536792+E6011e6057857f68d9b1b486571f239614b0707be@5441920c fb7ccc93e86187c519f6716c26474cb3+13714429+Ec4677bfcbe8689621d1b2d4f1bdce5b52f379f98@5441920c 972f24d216684646dfb9e266b7166f63+44743112+E1706fe89133bcd3625cc88de1035681c2d179770@5441920c 16f8df1595811cf9823c30254e6d58e6+17555223+E0febd567bf630b656dcfef01e90d3878c66eed36@5441920c d25b29289e6632728bf485eff6dde9c5+4366647+E7071644d29dd00be350e2e6fb7496346555fb4e9@5441920c 11dffe40608763462b5d89d5ccf33779+32161952+E7f110261b4b0d628396ff782f86966c17569c249@5441920c 0d36936536e85c28c233c6dfb856863b+22400265+Eee3966f1088f96d4fde6e4ec6b9b85cd65ff0c56@5441920c 03f293686e7c22b852b1f94b3490d781+14026139+Ef27fdfb40d6f9bd7bf8f639bcb2608365e002761@5441920c 185863e4c8fb666bc67b5b6666067094+22042495+Ee1164ffe4bffb0c2f29e1767688fbc468b326007@5441920c 4c7368ed41d2266df698176d0483e0be+31053569+E527d607c348f45ede4d8d6340f6079dd044c554d@5441920c ef75be5688e570564269596833866420+7357223+Eb27e68b0dc1674c515646c79280269779f2fb9ed@5441920c cc178064be26076266896d7b9bd91363+17709624+Ed64b0f5e023578cc2d23de434de9ec95debf6c4c@5441920c 5721f0964f9fb339066c176ce6d819c4+6146416+E5df3e33404b589fd4f2f827b86200fe3507c669b@5441920c 53df2cf91db94f57e7d67e4bc568d102+14669912+E64ddcf065630c72e281d0760fe11475b11614382@5441920c 3b045d987f9e1d03d9f3764223394f7f+11964610+E667868e60686bb6fc49609f2d61cb5b4e990dc4c@5441920c 1b83050279df8c6bfd2d7f675ecc6cc0+14904735+E91b1576015021d4debb5465dc449037bed0efc60@5441920c 16c366b5e44bd6d3f01600776e65076b+13400037+E6ded42f36469b5996e60c3415094d93b98d58d17@5441920c 6e7c59c345714f8d20176086c53d128f+5665774+Ef4c5716bb8c535d1335886f4ed8792e28829f531@5441920c 47c20b212e917be6923d6040053b6199+9646905+E875b5786fe08f40d5655ec0731368085d2059fe7@5441920c 6d56fc2964ee717fb3d168e0433652e5+4640161+E59be5ce3d0188761859f8f723bdfbf6f6cfc58b6@5441920c b62899c71fbf5ee6b3c59777480393b1+32455363+E2bfbdc56d6b66b7709f99466e733c1389cd8c952@5441920c 5c0390fc6f76631ec906792975d36d09+15940309+E0671c8fd6b2d8e05827cf400b6e6f7be76955dbf@5441920c 19be066d6bb9de09cb171c92efb62613+22466671+E2230614c0ccc69fd2669ce65738de68dbff3c867@5441920c 4c8396101d3fc596400d63121db853d0+13741614+Ecf2839221feb3d070b074fb1500544572dc5256b@5441920c cd29406297ffb7f637c058efbf305236+7619567+Ec063b1c180b6dfef7462c65dc2c7fc34b5756598@5441920c f68b644c6c02d36658e6f006f07b8ff0+23222064+E67594b67317452786c664f26808697d343d3316c@5441920c 42f58fb009502ec82e1d5cc076e79e4c+29666907+E2e27c6bef691333b19269570bc175be262e7b2ec@5441920c 384e1e7642d928660bc90950570071b7+16511641+E44951c3c7b111f06d566b686fc78dc430744549e@5441920c e200de735365bd89d42e70b469023076+26095352+Ef9566086c4526e88e4694b55cbeb2ed3d229198d@5441920c e809638508b9c667f7fbd2fde654c4b7+26536426+Eedb7bd609b7d22df73bc5b6031663824ff106f5f@5441920c c6e13cc51e2354c0346d4564c1b22138+5595242+Ef4eb609230d6644f1d8626e186f95f9b784186e3@5441920c fc6e075d862372e6dd4d438f0c339647+524636+E28e5d58c5feed7ef5e11869e16b00666424f3963@5441920c 654066ef6cd1b9ec3010d864800dd1c8+20166756+E655b286e729e5cb164646314031f45628c914761@5441920c dfe8df7f1f6d8f37667f275fb0f16fe4+10195576+Ec7b5272532230b29ce176629dbe6c9098f482062@5441920c 0b3e18ed791e551bbde5653487cd9e0c+26057104+E95309d4ec6c56d6490946103224e8e6d35622e12@5441920c 9f453ed53b8be18d3538b9564c9d6e2f+14129943+Ede61011c6d265c59417889db12301c712ef6e375@5441920c fd919cb4313d5c4d3e6d36ddecb39d9f+27262406+Ee7dcc78b62b26b179f6cd05bb6c56b6d932f01f8@5441920c 2371986d9b195513d56d7d8b6888fd13+11366564+E487076c1c0dbbfe05439e9b7506b3d79dff8e3d7@5441920c 19cc39fb80e4cf65dd9c36888261bf6c+4264756+E5d56331cc97d68d9cd7d1f942b04be3fd808c640@5441920c 622c38578f1913e0d1ce5db993821c89+6746610+E95f98718306714835df471b43393f45e27ddd9b9@5441920c 3836977b216b56d36b456fc07bd53664+21620366+Ed358c40e313e1cc97d3692eec180e45684dc21e5@5441920c 738636b97bc221e7d028bdb06347dc16+9166469+E76e010db792235b2fe1f56f26037638570191f5d@5441920c 56605f61b621650d3df04831649d2588+6326193+E1d9d0567e8fcb93990f7c4365f92742983e6f69c@5441920c 2125e15df79813c69497ef6c0f0f3c6c+12757371+E30cbe534f649db7301496eb203711dd9eb3e9ee9@5441920c c61de805f19928e6561c96f511fedbb4+12157116+E756df376e5bcc65319d062bd10685df117957004@5441920c e32dc879179c2d507bb75ebd015d4d26+10261919+E2250d07188228888c8052e774d68e2918f6c4c2e@5441920c 6d2d0e3b6984940858e36864d571eb96+40669605+E2bd8434ddf794691166b1556e47ef8f7b636c920@5441920c 65603431e7ded48b401b866d4c8d1d93+24190274+Ed2c84b40dde45d8b4df9c696651c4d8cbe02e019@5441920c 1228e02f7cbf807d8ed8b1823fe779b3+10020619+Eef06c59626f88b5dc9b741f777841845549d956d@5441920c 7367b338b16c64312146e65701605876+44636330+Ee6d463f6d719b0f684b7c8911f9cdcf6c272fec5@5441920c cd8d61ee8e4e2ce0717396093b6f39eb+13920977+Eb6c4f61e78b10c045b0dfd82d9635e45b6b01b5f@5441920c 28079dc5488123e5f9f3dcd323b7b560+22369141+E077f18b49d62e4d88ccc78dcc0008e4021d7342b@5441920c 56bf3c8e6c6064f6cb91600d29155b2b+22616366+E920d258e698cd2e7e66d9f78de12c87f62d472d1@5441920c 49f686994d4cb5967d19641e284733c6+26439412+E9dcd733412c06841ded126efdb30542c4f932587@5441920c 1ef6646ce8917186e1752eb65d26856c+4173314+Ed60dc1dc4b9ed74166619d66109f6eb546c86342@5441920c b24076cf2d292b60e6f8634e92b95db9+39664156+Edf615c5203845de38c846c2620560664ee6cb083@5441920c 576e06066d91f6ecb6f9b135926e271c+11123032+E9d147b4b89c947956f0c99b36c98f7026c2d6b05@5441920c 7642676de1dccb14cc2617522f27eb4e+10756630+E55cb4ed690976381c9f60e2666641c16f7cf5dc2@5441920c 77580fe91cd86342165fb0b3115ecc66+10560316+E99463b8815868992449668e59e41644b33c00244@5441920c 1c506d050783c30b8cd6b3e80668e468+35565426+E67c9d75c946c5c6e603867c66ccfcdb45266fc34@5441920c b0d8e3bf2d6fc9c9d067467749639c31+14197061+Ecdbb94e40090d099c847952d2f21de89803f3169@5441920c 01605bdb27b06992636d635b584c5c2f+20756432+E36de4fe4eb01fdd1b9226810d21c8f62f1d65643@5441920c 0c27885b49cf5589619bd6ff07d02fb2+15792191+E23bd16d3bd20d3bed3660d6fd035086d6d5146d7@5441920c b0149371ff6e4b097561cb6de4b5018d+22249239+E4f207f62d04d6d847c27e2463f69b847676344ed@5441920c d6fb819c6039468f36141e1344675379+16449706+Ecfb1156101edfeb2e7f62d074f52686d215def86@5441920c 09d34633511ddbcc6646d275d6f8446d+29052525+E6bd7fe2d67cec4ed4e303e5f75343e4b45656699@5441920c ed798723d587058615b6940434924f17+23966312+E97c78dcf692c99b1432839029c311b9e66ec51e9@5441920c 29f64c166e005e21d9ff612d6345886d+5944461+E004b7cdd000e8b6b82cde77f618d416953ef5f76@5441920c 8610cd2d6fb638467035fdf43f6c056d+20155513+E76b2453644c8624f5352098d3976bd41ccd81152@5441920c 64fbf1f692c85396dffd0497048ff655+26292374+E3d479e00158992e9d770632ed7fe613b801c536d@5441920c e7db466023228e000877117bf40898d5+37776620+E8268e86cf6d614e31b3f89dfcb73cfd1f7b4472d@5441920c 26f844c3000746d76150e474e838876c+16720695+Ecd248063ec976663774bb5102068672f6db25dc8@5441920c d631188d8c5318efbb5966d96567162b+13059459+Ee8e8b625c936d9ed4e5bfdd5031e99d60ec606e6@5441920c 75e196c3ff8c902f0357406573c27969+7673046+E3fde8dc65682eccb43637129dbb2efb2122f6677@5441920c 90d0f062f153d749dc548f5f924e16c7+5625767+Eecd6284d567555146616cf6dc6cc596e76e30e62@5441920c cc3f072f71cc6b1366f8406c613361f6+42976743+E55561d73068c4816945df0039e80863880128997@5441920c e74b79c0cbd84059178c60e8016d113d+13609906+E74850d9197693f46e640df4c7bf631f5cd6fe7db@5441920c 186706b6c31f83b07e7c60eb358e93bf+11966262+Ee4e0e578278e9288bcfc546355e16dd07c71854b@5441920c f85c6bc762c46d2b6245637bfe3f3144+17595626+E780515682f0279edf3bc7638e69dde8d5c87eb5f@5441920c 80fb6eed15dbf3f3d88fb45f2d1e70bb+6567336+E61709663412711e6bcccd1e82e02c207d65083e6@5441920c 55d586d9b4e661654d46201c77047949+7406969+Ef65e6ef6de723634d7ebc04b8e8c787760940948@5441920c 6fc45eb907446762169d58fb66dfc806+26345033+Ebf58596e6096dd76c9ec7579e5803e82ec7ccf66@5441920c e398725534cbe4b9875f184d383fc73e+11140026+E54668ebd22937e69e288657134242770c1fdc699@5441920c 69b586521b967c388b1f6ecf3727f274+9977002+E6eb4b63de4d17b50866bc5d38b0ec26df48be564@5441920c 2e293570b864703f5f1320426762c24e+13651023+Ef6640563ec496df42bcfc696986b6e4f6edccc68@5441920c 462b1eb00f462e161f4e5ce2bbf23515+19646309+E47ec8fb615747c6104f7463ffe65d1f6738c2e67@5441920c 7f8eb265855e458e6bfc13789dd696b7+22406679+Ef3cf31dbb3fefef455f62d6b5c2486500f327398@5441920c 36659b0e79c69296927b418919561e89+24370117+E66e94cf0be13046deb186302cd666d5300908029@5441920c bf6dd822cfbc6b90f986e5f43d500c6c+34354522+Edff8be044ebd69391cf282451659660d5dc6dc12@5441920c 2267fb579f99df6b23290bd2e939bcd6+12153797+Ed3de8875c91d6f346fe320b20670c410f46e7ede@5441920c dd66288e4f7ef394f6ed7e9b73ff5178+19120741+E3860d5c83e021eb3646e5884018ec3dd59d806b7@5441920c 7f86957074e677328be7538ccbcc747f+16676462+Ef6492f2cb4dbf9d73c1e58e2d0d85b0dd2f18402@5441920c d7363e073e178f502b92e053369f40fb+26125462+Ecf329f93efd1ec34f17edb991de264b9590c88f6@5441920c 6d64dde62f62d6febdf6f2c66c0220d8+23263164+Ecc22f32322cd039cce602e155bb530ebedce7b49@5441920c 7b70bebe42067024d360b7216c55d7e6+11436933+E7b70998697b46b0840836219c8e37e6d74906656@5441920c 3e6201706ff76745189f1636d5827578+27434607+E5204e6cf46e581b019661ed794674b877f7d3c26@5441920c 1b1968d7d8bb0d850e15bf8c122b1185+13431932+E28e98b072607648f73c5f09616c0be88d68111dc@5441920c f8ddc22888e3fff1491fdfc81327d8cf+2633555+E1b55c1417c2c0bb2fff5e77dbd6ce09e7f5d68bd@5441920c 9f200cd59000566dd3c5b606c8bd4899+10166739+E88797b1c2d44d6c6b6c16b6e2dfe76812494df2c@5441920c 65f26cbde744d142d8561b715f5dffc7+13335963+E13e86ebb6b426b1f4b6546320f95b63d558678f9@5441920c c89cbf812dd061873fdbeefcbb7bf344+6763176+E13b1765c5d3f3709605ef703c5c41bc46f25ffb4@5441920c 99f663066b7d0dc6f6e355eefbc64726+13444650+E8f607654b8d1fb72109b2e3eb64645202111ef2e@5441920c 6804c29fd6b3ec351dc36bf66146610c+26266416+E106283d64058d0c8b15061eee6d2059095767f7d@5441920c c23c67b4d1123fee2d8ed636c4817fd5+16376964+E392625bf396b887186e8200d94d8c7e392352618@5441920c 3f7640ed561971609025b37696c38236+14116164+E55239788883085d7f854058e090177fd10436258@5441920c 4f4014cf7cf09694c6bc5050d08d6861+23692725+Eb40f77014747eb8756606581bb6cef6665bc1e92@5441920c 0f46b1e0e8e69d0ec0546666b21f1c23+10507763+E173fc49b601c3c699d7cfce8c8871e44b371e6cf@5441920c 24385b164f3913fb234c6e3d8cbf6e55+27625276+Ed26e6d9e6eb59b6cf51c01d4b8909dc648338906@5441920c 0ec3f2ecf85f63886962b33d4785dd19+7026139+E43ec8f5ee2bf4f3b639ed66313c2363965702052@5441920c 674e2b084199c6be0566c29f512ce264+27711533+E1752f5c20c69cd33e669012632cfb2b93e1febf8@5441920c 8de5446ce99c95842b63dd62f2836e35+6793207+E808e94501ce9cf2f0b694f16ff261d42792dfc34@5441920c ecc3b274850405ec6531982414c634c2+15405916+E3c45d5ec865de3c34bb7e14e5577b7ec99d50268@5441920c 4c3b28e830f55707601378f6b314bb36+9160724+E6c42dd49736833326cfeb59003340d99d336b85c@5441920c f217e6338e5be409b309bc05768cd692+9467601+E33296cb0476d39648eb3518265241d2e58667c69@5441920c 1c33d278e00d838960c35365e8b211f3+7969532+E976bbcb318e35b425276d16640687cd30c0f6513@5441920c 45fdc6257f4601f5e6ddf2c3f3249453+24739014+E37fc9116462386d43647d43b1f24301fc2b3d2ff@5441920c 42c619bd934e4ee7876e6e62bb013c8d+26941562+E22061d93633689db860c97d09c2d428e0bc26318@5441920c cef567d31d5e889fc38f0b1c8e10603c+3036311+Eff049d2e8b04646603c7307d8427ec384dd5636e@5441920c 6d919324cfd4489696661b0c3bd2046e+7761096+E3d0ccb506d66c4621d1563e7f301d9de5e306ed0@5441920c 4631f15b56631ddf066623240ef60ecf+16709476+E125d603e61f05573e9bc6d15d64038548be25646@5441920c 6c897d794f5e90b15ee08634c3bfbef1+22602265+E65c0d239fe02411d4e688b0ff35b54b5fbf861e6@5441920c 26e1e7c8d16d0ec9335c8edb01556e74+23405696+Ed77c8c87b739992b6e2f4f0bd813e3877c029646@5441920c de5607856bc6965b3d689d9f6c739dc6+14457362+E16b373fe771865bec4e26e0c5b86e3241be55416@5441920c 9c96247f87d27cdf351d10424fb65154+11220750+E5666f47b25b3667bf32b17cf06202016edd96078@5441920c 6bb96d31bb0766150fbc94ff08ec1e50+16561466+Ef617977d6fc4b3b7606056e7744f61508e1f6dfd@5441920c 290806849f83631376637e012d63c055+15634314+Ef56d98c07c837800ef7653b9e74b1c868911c512@5441920c 917ff996f786819bc13747d05796db8d+26147265+Ebd9eb6985b39beb62d7cee1675dc88bc469786be@5441920c e3c8b5f953857082274364d3867fb56c+11193151+E39798993b68bcde100412e41e046f716cb576fd4@5441920c b0ce9f0bf1db246f83f961be4789b2db+9599462+E9d8bd12dc40e9e4665e4f33206ce9d4144b5c48e@5441920c 77d5f68866703cc369796f6d56c4d564+9625154+E6076126e1811c6e7b05c8959558fd35be4d9336e@5441920c 7b861b04ecef1e4260f42febc076dd48+46677445+E979196bd9bbd7456963e8f55564ecbe16ff3745f@5441920c ffb4f46254cfc652517e153438489038+12795653+E43e6ec68c5276d6422c66b077266230772849035@5441920c 7699462d29f00f611f35891127e16031+27123199+E09eeec5c1612c40246b21e26b65766ecc59bcc9b@5441920c df706e0400506e210565939e04539eb8+16632721+E3d404cd76de417682560ecf97b5c7f821c18148f@5441920c 1c9d96048b663c625fd02658f6f75c7f+12652756+E97cb664d41f2b9c69f9fe5667c12bcc266b6d492@5441920c ed360b6b945be71391e803353132c5fb+5706666+E7e4162c6cc3862322792cf91d76c719c84896c74@5441920c 24b7bf83c6b60fe6cf9746c8d16b86d6+12566075+E0d0b95ee04f865f5db70e2c80d35ed7742d20619@5441920c 9deef070820c1ecff87d109852443e97+16946677+E288515ff55d2b49754bffbde646d6b9f08981b66@5441920c 5e57630e60dd29658e61165360404fb5+12209370+E0762d4cee56b876c85ee0d2fd468649640561070@5441920c 61c7e19f7e96bcf59bff036887e5e755+17916606+E92d286ed713f8cb36d44f6b0346db71b5156648d@5441920c 878e7f227305c5c89ddc057bdc56ede5+24643337+E214637662b794717e65860d89ef5bc35f3f43d10@5441920c ef1514658c8f004fe640b59d376fdb06+3264756+E2b6eb6625c08c54758676006f634f9d09d9218b6@5441920c 485e4d6249b959b57226eec66268d074+4102134+E1118dbb1517f7323387bf970ddd5457c852353ef@5441920c 06d4b5ce44510d68dd154ff45203448c+19703325+E65bff4376436dff5c5601120e7c7138cc78eee61@5441920c 6d6616d27e10b3d0b562d154b6934eb7+11554223+E814476dfc3d4839453633b5538f76e11d365cdf2@5441920c f81f6f1ee2b866edf1e866c360c9decc+12130664+E3f3c05664668c4573244d3ce9ebb32356ec78d00@5441920c 66fb6db666667e6fe4b644d414643225+5642000+Ed3db35e5034c66e26323c3711b3bdd9e0c30b9e1@5441920c 5bedd5d1813136695b744e6696bd444b+17354621+Ed6c692158452b91b00e4f7065fb4d57945c6544f@5441920c 041391d37c47b66c064f216c76967c1d+7546724+E225d15c0700689d941be9216136d5159e57617bf@5441920c 0b3936e98635485dc5c39c091b1e141b+30306549+Ed8201dc4b2f19c6436b27200cc661160880f53e1@5441920c 87c955bc76e6dcd602074cd0b61ef669+19466657+Edce058995064b4c6d2ee4b5fd77634ef612fc4e2@5441920c 5863cf41b6d842606191f91266766ecf+19566732+E35547d8c39d6ddf6f0fd663ef6207d369121fd2c@5441920c 4b2cfe879bfdd4f5592b2948e1f12f80+16726166+E0c34f334513cfc42834f2f1b8bf3c2ec320bf9cc@5441920c 18fed9e859f59e23181668e4143c216d+7297044+E77384d2014fc7f1e460436175b45bb23678c0f70@5441920c dd1ee9df0750267ee5bc9ef6f29b0632+13453405+E45879d6d0f51bd868f7361809df00e383b2d83eb@5441920c f3e82d6578cc5172dd9f264f50d8bb42+20691242+E246dff090584102969751374c13e36510ef96feb@5441920c d68c62d920b706612d32f31727654479+13969727+E0428790ccc219305dd026886526fc5f41505ef67@5441920c 672f554d523e6939c88956610d8d66d9+15929956+Eb0468436beee5f8614d96765e75c628443d04832@5441920c 03690d1333904fdc508c57f33c715c3b+12006715+E3dfb288e160d2920cf92e3cef145d82d8636d807@5441920c d7d5d48c6ecbfff8edf63e21c8ee1680+6976746+Eee6cf6450806f2d68c7ff61d16ff0b9b09bee55b@5441920c b206cce6b38d71c626fc6260d60de055+16617309+E5bd96be2db6bc7692b8e7166fef6741635fe71c1@5441920c f82bc9fb241fc9bb1e9403660f31e963+26602130+E23677fb52377535f6f4d98371640701007467dd3@5441920c 60909d87315fc866ce54161907883f86+22761626+E222d02645d114b88836267760cc5599064dd8937@5441920c 5938d2c975658ed73f676cdf8e2df648+7096657+E6d5533fbcdc0f54dd094cf4de638c4cd3020bf04@5441920c 4b8c87889c09deee98b01bf9ec143964+26067196+Ebcb681616efd85c46893be63dd6663f5b45695c4@5441920c 4e7f06d06fd613f5d50dc3b9626d01de+10673992+E66fe9d65f3f18ef2fc74c6c766e04c6826060c21@5441920c e016be89b3607dc2c6d84703446096c6+14647560+E67d21749bf35c936546c2816e658c8ce4fd4863e@5441920c 65663576005d0735780d7783d27fd612+6567442+E3eeb256c414f59c671484666608019515b6d66e8@5441920c 8184bfb40466690c3c7bd33cf2001b7d+27369311+Ed3b2d4e52f16cf2c20b95e1650f0b69671b6767b@5441920c 28210e98e4bccfc0c8c881ee65dbccd7+9264693+E6780fef94c00c22364661b4df03db1894b65b279@5441920c 7d635728d6d3f0654491e73d06e2760b+16320752+E89b121f6c09e7f188397cedd9ce53064630e4197@5441920c c355555c484c0d41d31c1496bb0f88d4+4140293+Ed2ec40601643f992424e6042610ceeec4f926202@5441920c eee46de26c233081986fcc63036f6e87+17266099+E643f07bc7496eb97beb2bbdd74f78d9c7c40632e@5441920c 6bf27eb8b36619050c0246b26d541397+3060756+E9ed96e63725bb226e6717733062d92c38d0dd416@5441920c 17e7810c048bbbd3837c74253576c064+3260426+E660edf2b267bd1dfb1c70d25ce1173d99b572435@5441920c 633b2f33c40f13b691d59f8b64543ee9+26136225+E65975c79c76fedc2d8b92c2d8095845996c656c8@5441920c e5588b19938ee85458f1008b6155ff80+45662056+E5fe59f043d3b8e6f1ccc6d92e19ff6c6bd6e2d2c@5441920c 14b6ece5c233ed08c8343665bbc435fc+10447960+E6009d59e556cf6379ed6bc849f180d1cc33b3068@5441920c 1064ee1f9f687c0461c5bd686b612ce4+6564566+E7cbf7c65eb90855372605b5452b6265366e64841@5441920c c073866fd327e646c556d748027d6cc6+6396676+E8c404153f6d5010756968c6b9ff619bcddb1e1d7@5441920c 1dd987d82e5f8d23659cf23db99f6517+7956724+E18d666c504486712bddb5f8173658650c7708182@5441920c c4eb6d77298d6964f9e862e809463521+34269266+E1e466382fe93e2103395fedbb57bc5e2826f482f@5441920c 5c621f017e2e17260b15e13d6d6102be+13762411+E5293993d8891eed812c1829096775c9129d66d86@5441920c 706beecbdb9f413d8456e05b6744f6eb+3947613+Ecce55b46196c75ccfb06eb9b392e53d9f1c71c18@5441920c d498f6f76978747843767963f5064309+5537714+E2885742de6412d62b47c33bec68d8d9f81f9c09c@5441920c 2266396b65b97e348973206358673f66+24305632+E2e0ec28566c629333dce5f41e47488f4d736f018@5441920c d91969572c86d6b14636f6e3460bcb24+17507515+E96fb6850f7fbb4d9c2e0954be44635896879976f@5441920c 11b46690ee6e9bfef0c4026d856f4670+32626524+E361d099f561efd303d2e24182ee09327ec51657f@5441920c 2361c32669d0564e52d336f85923b61e+1010299+E45038369c554e6b30b60f3ec580898792163d919@5441920c 858bd2ddeb56d69038b78d289ddfde15+23454636+Ebb767b2668b5f9f61c4de733265595f1c074e606@5441920c 91618b31768711ec4b19dbfcfc7bb98c+16017355+E876f5f62b67613de0f79e60f245cb0f02f017220@5441920c 1bb9feb4c6ecd90cf1d8e061fe9967b1+9792746+Ebee666de05c3811c76620f4d9f49cc7103f0690f@5441920c f76ed53563936eb324feb4fcf9d2e44d+533647+E59361b31266d7566c00ce339629b5d1d86863cb6@5441920c 47f61e664eb4d68364d94971f0446206+1064656+Ef226fc40f66666690e640c125f636b37c6e75682@5441920c 155b75f465771d25168cc2f723426908+27465637+Ef6d455ccdd7350f6d8eb036675b046bd531f694b@5441920c 189e6923d3e6810634475b6563ff42d0+12707353+E218987c1f65753c694feecf176253ccc353268e6@5441920c 345957000ebe671b86130e51896d8694+6632970+E76eb72461dffd0b03ebd0287b4bd4df60fff6019@5441920c bb8830d56f6e8b0463c1897f9c6c9b68+6746794+Ee569093960e68f65b8bfcf0660c0d51d8e316507@5441920c c1c82dbc3246d4994c7110536532bd3f+17732191+Efb0bdf49337261801bd36e7f961cc766bb258d6c@5441920c 3469b89f618cf43d6964c89cb7557360+15491375+Efb4f84bd36776264d5b66193cbe06700c9c36986@5441920c 1c6c8cdd2b55b59763484fc736fcb2cb+20295749+Efd1b1e16c26825e6be2f0086e5956ffc2cb86186@5441920c 425eeb625e0e6f78640cd646b81ff96c+27117670+E6c651bc6fbf0911c5f0cfb13cf46643234cfd962@5441920c 467b40e186cbe66e68e27b497c146989+14464752+E6661978e64f282c9673fbf76c8c28d447de95571@5441920c 215e9957c31b9786166166d3066dc8c1+22592925+E24ec6bec163688076c95e6d575cc43c4d2185d25@5441920c 8e6d9566f2e6b368629c336c9fd6e0c1+21043993+E60f9744737815de11b5cbbf7d2b9bc26197710c6@5441920c 6903b3ef7b72b5c437121c8d75ee5f00+6526756+Eed896e26d13830cd0de7271e986638655bf936f6@5441920c e99d862823e5647d428cf03c85510dff+4646274+E7f7e0d272568f9d8353432e1d1284c6e99179ee1@5441920c de8752933c71e8e4912367c396286d59+19571326+Ed6eb12d8d1ec809bc6636806c89f0fc31b76e49b@5441920c 42b9673e467681dd1b75622d5871022d+12923669+E6638266df36f80ccee9b177392378fe0174654ed@5441920c 6738766901e6522d122065eb602706f8+9921926+Ee0506f3116684358651481b6f6766b6d61e4df36@5441920c 25ed8c9f9b7fc61b3f66936f6d22e966+2695507+E24986eb797bd7e2ce75f8cd7fd13502bd1db0900@5441920c 5f63716d6964f6346be68e50eb3477fd+11292446+E6d40765c1ee54fd31d239e1e96c25d6d964e6e33@5441920c 646ed63541be7c4b197e74200fc58563+40629656+E3228f646ef6d86dfb63090bc1f4540534fb12809@5441920c 2bc96d464c08c774950465b994786463+4060756+Ef6418662f5bf612877bc0334972769d5c364bbbe@5441920c 074f412860c7143944662f3579e8cc96+16610667+E7d989e4216744576f348473d58cb5102cd3b57cb@5441920c fdf162c24e1b743db60644c910bfcf26+29170320+Ec6c6b955e0fe664690d2364446326c2f16279321@5441920c d1e6d9e6512687494cb66788d97d6b76+21574362+E9e9f63bb64f611c623604e6f6f0222e0c8105236@5441920c debdb22c0be9d5cf661539bfdd628421+3619563+Eb95f6d2052bbc63bb931d21fb518f89531168e2d@5441920c 1b3b785b6f585c9f46c8b932ce5ceb26+49161531+E2f15232081e450fd4efe9368bfd8bf8162046667@5441920c e336b53894f0543d59963105e9678367+19746144+Ebf3c79b229c275ee7e1201257605016278153d7d@5441920c 782f48c017169e53d2c726d046dcc6ec+10946735+E9e78046511c67ebe2b39f5b21622bddfb87069c5@5441920c fdeb6225b7463435cebe00e7f86df276+6376465+Ef4599c2d6e757f7f66579b373e9e6ef0ed74b62d@5441920c 32d626f756c4cdf566533c6b2df652f2+26661567+Ed4671f20388d6576565fd26bc00d53f0e38b6c51@5441920c 14c4e60bd3fbded9dc8d11d6e970f666+13661669+E0d589b83806594837ed672319ddfd74f3cc39ff9@5441920c 77886771777c50587e02dd08866b75eb+13501427+E01866f494dcd7dd4fbe7541df16529447e52ef6c@5441920c 8b3bf3e5f6b6be1d667f36d1784367eb+13677551+E6b241697c8d0c97c142fb695936589c1945e9ebe@5441920c e12686bd46818f07614c0143b68802fe+15666076+E24458761c577527694bb99ff659b96c954dbc3e4@5441920c c710454601fb0f6e4d03d6461fce5f17+7996490+E8e9cc9e865e420e3e0cb0987f106665e80e7184e@5441920c 316eb301c1ee9cd9b38c6544cb7bf941+6053236+E04118416885186189d00220842078fdd82b105bc@5441920c 1946863de487f91790e10ce2d63deb4f+10726254+E1613e538b89d50e662650196b2bb46060e46b325@5441920c 7e6debd8e9fe0f58f0c0ee19225e4664+11356746+E15749f35c8f636eb7666f8d62d32f179c7f2b443@5441920c 62d6d9202fc0cd2099157526b4977b6c+7600427+E5363fc1d6f6c9ec60576c454be6e0e026c638644@5441920c 80f767764063d69fb042e73741108330+20722736+E79223662b666f482c76c074de7c948d9b81e9eee@5441920c 7de230cb3c601ffdc306c656d729e766+13729019+Ed6839fff29b73d5b54c16855f0cb57ef1f0d5dee@5441920c 566eb88cf65d80f8def689999ef64367+20246913+E4868dc526d88506ced164b48b2cb6ce669820484@5441920c 27250e8f350f3b51c756d68e47e2c980+26945676+E8c606e26b483c6e93227776776b116e63c7b6607@5441920c bb9e9cd086ee769366229cd0b32b5c09+3364670+Ef63125e4676b66d764234e76f314863e7769e3f5@5441920c 50e50111ef9bfff37663d6932f9b72fd+16155754+E056360cc57665896b629cd38fe14715621363de6@5441920c 72e864cd512f786c54b9f07646e66e37+12762477+E6bd9bff5c2926b09dfd6b66c2e969dbce9f53669@5441920c c339c751cf7d5166c30b8b21dbefb69c+16572364+Ef279e41366b796bbfb333ee55631cd9dfb6e097f@5441920c cbb37c74cd1f688d1c9756cffbfee897+12456663+E523b778eb6355bb66c2f5d4773d775bc6df25dfb@5441920c 819066f13ed2c71947e3f647656b576f+14524669+E62b3c65fee64e372239593516c64d60fcb850d75@5441920c e3635e4290543563388e94e1e6109729+36661662+E767f7d2e1298f1ef565e967e6170f88f7d6ec9f1@5441920c 2ce76730ceec8d843946f809c16f6f46+3149045+E882e0ecf259166b860f68dc6fd844cdce3fe49f9@5441920c fb2814493d1c484625bee373d5369cb9+13700211+E6be1eee5409d867cf0327d762d7ede7fbc296f25@5441920c 6ef39899b0ce52e83964c55f466f7021+7529724+E3095671946451ef2d9b129106c26f1e9515eb60b@5441920c 36e914556f2c8d21b82b63578764e811+7950542+E329cd0c0b244ff75d31782f2dbb7741619b24861@5441920c 895c6d874d1245d8e66455604fc45d3d+14756600+E764966661b47eb9946f1964e5ed060f623240695@5441920c b66ff865ef7d09bb19966902e62429e5+16443596+Ee56f4778f3b067103eb6bb8e0249fed5133749b0@5441920c 3e76f1361961466b0b95d3b6f8ece285+20106669+E01c84b2e28e91ebfe917067bb6671061c8db49e2@5441920c 63953f84933eef8bc8bd15c5d560c522+20056363+E4c6bd626c3b008116064f13694d49844e6e656ff@5441920c 5964964ef7c947f1c185073125669465+2567406+E064d861f4630b32521588b17290264c70f3cd71d@5441920c 379733627e446179436f327832659951+30547504+E76c3833c4d3698066d4eb966d179b85bc889e628@5441920c 3358c02673c23b84c37d83e469c72f66+21562054+E6008936e0c5343533bfc19f5c81ff58c3e2925b3@5441920c 46cb194289db37ee376f4f3346de0e04+27395356+E6db539216c1b433314f27bded4c6cf0078bfce37@5441920c 94310de101827648d6b3bc3c89708c59+26365676+Ee319940fb28fc2b11801e3019bd84937e2248074@5441920c 42220345631c336b5194ce9b573ed40b+269200+Efe4d5267e1d56103455663b90c06d54622e0641d@5441920c 2263e6126061fd7681b1d7e22b9f6e14+5237174+E51317e2730be6fb316f2b2b6e31d2913f4f37676@5441920c 07642351234b816b15e150bb6bd637ec+29727146+E66325bb50e67ef4de1d94737653dbb98761c1e66@5441920c 2fd5ccf86cbbd0e3c3f366d7bfee56de+30907674+Eedfe3e86d6243ddf6d5ede6c86604e7e310283d4@5441920c d6859cec4d9fb1c68e391840579b56de+1504656+E69c673e18f46659560ce19e24cd642d7ec4cb3b7@5441920c 49620b9c06ec234288fe02c59e694928+14943044+E4557ff4e2cd1800c94b296ee059f895660b0d38b@5441920c 1e7664d9f69c30178124676004c5622c+33721037+E16ec6ff518bc86565f4c9dcfc0656e38cf2d47fc@5441920c 17ebf9c6bc4ec665ce79750639272662+24605551+E636c8155632762d667d6c9004f6738f927dd5979@5441920c 342b663668c683fb488c62ce8568b618+29376907+E156b0293e6de6662cebb0703b9e2b37386fd116e@5441920c f0ff7321084e5fb26b047c29b787166f+12633635+Eb57428f2bbc765e1391c660e6592684e76f624f8@5441920c edfc2352776c326d1425c8f75206518b+14797426+E136b15d57166c3791c3cec25f2606868be3bbdc7@5441920c 19556e814b8696d174614d2635efce37+13760102+E8e64c18124f98b3f0d615b89b4bcc0db5345471c@5441920c 25764e17398bb530336f104fe1f16fe6+26794272+E6fc3ce18868166e546e46d72fe289455cfc70834@5441920c ed98ddfbf7181c16fc299ee261fbfc82+10201924+E2de330f0e91b386d0d779d21c3918e998cdde6ce@5441920c 77eecfed3522b3b96d26b645e3367fb1+24124636+E2332473f67efcc195ef87657368074fc7b600642@5441920c 9bc03661300986db109ef2626d3742c8+26615557+E68ed6cff0f9894c2ec3e940e0c676ccf99b6c0ff@5441920c 152316dbdb21124ed53e3eb985b94dc0+22145236+E658096d502e9136b69b1fcf50d5064613dcc7d0e@5441920c 561c751762166c7b8fb609601b9f2f48+7311346+E81d4d07984d6c5e974c15008d4f92d663c710388@5441920c 012c01572b943bb8466fb8116e57e60c+12577740+E1c98f4cd9f1760b062bbb20bcc0131eb9cbf5821@5441920c 4dd985e1e9728f9d676d9d526c0225cb+21506140+E7ef21dd62f372fbf66c17e6164064bc9c1283863@5441920c 626622416232e782cd0874f9fc41e170+52369+E8ec7e615f231dfe25b603f3c178460c06e624f6f@5441920c 6b7e084ec85bdb5633ee1355933517eb+5076969+E3251c561406ffccf6f6678054cc66308160672b9@5441920c d944332019b54e4213694d720652f837+31190176+E51c7c1b974617f8711d31f1ed3d554dd69708b92@5441920c bc35e4ec4f310481df053878c99e2028+41160366+E72e6fc6c8996446f8428889039d6382c3187ee57@5441920c 32b116162e37fe261fbf44699d161bdc+23615045+E7616236b140e626104830c0bf9b63c3632defc9c@5441920c 3260853d69d0f6b96ce5b079b1f1037c+34031699+E614c898376081ef614581fcf012196259b247f1c@5441920c 9024866876926291e291e983816cb080+13651503+E44d2c5f757e5ce51df4bed90d213e67280c08cbf@5441920c 7f7352234c5c86d70eed25447b6f6e51+1996046+E68d1c68b7d65e0697e6c47285061b36474bc9848@5441920c 0866e053769fee5e5eb4c9315d6bc5f2+22692591+E44f353b0622fe8378168c3cc6684ee351e0105cb@5441920c db74b6286949f3b1fc69be2083982e48+6672354+E2546bb731323d421439cd1c6e426dfdc0e6f3184@5441920c 1f6e9090bce4972b5371f66be3dbe365+13749361+E6276f45e81bdbc0eee34e591e76b38385ed87108@5441920c 8495966c987b24d64e8f23261e40773c+16660930+E6b7e063904d76d68b68ce542095408b362230e93@5441920c 222648c113cd8d52179954bf684d5626+11036031+Eb563b11617cf4f44d7c31e51e50d17e0f398f063@5441920c d878ec2cecd3470c7dbf4291653e6c90+13412650+E0dbd46d19e8b6f8c66064196cdceccf5b762727d@5441920c 7658c35e0b91464508f7133dce6e60cb+14313555+E54e6f8e766224090ec6c74f776d30ceccc3de46b@5441920c e8789734411f44661e0fc74c1c0d36f6+33635703+E470928dfe26c643e0603f7630526232621ddc4c7@5441920c 8825382969eb6c5066fe78997e0c7bbf+469634+Ed6dec1e7f6886d2bd1efbd8c6edfb22edff74bc1@5441920c f5552117005f6c9d736496e2f9030f5d+11377056+E0c2be5653d1776957700311e5de86764c636bdc8@5441920c de62f65e30719327fdeb326c2f16d698+16346545+E5d6e6d619f640363f167467b2de9c64347e63768@5441920c 3cc990452997b05b51ed170d291f9f1d+21127772+E281769d6ed0579760f4f2342c1f9bc76618c8cc3@5441920c 13c43c4e049c7d067f0f1dde01648303+1059366+E3f19eee97b53b375756ef3367b86deb6077c593c@5441920c 7d62d6e36364e35252710e47b06c54ed+6964270+E8030563b53b8d7d6c4d127c2e527e6f2ef56e98f@5441920c 7255f3e557e3be60e6bd18054b360f99+20073973+E2d6e29ce1c66668b02f075d99194392b83bb67eb@5441920c 32289c50f7dee66d59260463d7b85c7c+15769669+Ecd9f070e6f3b0555848c8506610997600db07b15@5441920c 6603201c20e0c24b9602169b3547381c+9756229+E3268c74ff8f0f67d1cf1d10c01dd9e2332dcec21@5441920c 061c6b2528256682c7b205b0f0f9d69c+11469333+E960692b62d3d34902fc765048d36081bd58b0e75@5441920c 80b649545f654616348cf66f4dff90f0+11074951+E49c53fcd4deed6b62e3d292e66e2948716e7e1cc@5441920c e736c8b66c29160f42d7ee5bd649e636+26145091+E436426d265d3d4d65658e6b39405b82d308639fd@5441920c 961e212b3d7f9464c268692761090f6c+20545569+E4606f1cfe9cedef085404f8465b915190c8cce76@5441920c 761fe39e125f6f19585464b661706631+13562476+E33877770d62273e62e345f52b755f73fd56c59e7@5441920c 241691bc053966df9f226e308c46e36c+19737049+E86d7f2325737cbb78d6ff61b583ec96fc4c8d0f4@5441920c 6e367eef8cb34400d2b43368893c81b3+27529030+Edff31b6b50ccddb0954c28c8cf38ecbc86417510@5441920c 05555c5fb49bedcf63ff878f1cfbe3c8+15452164+Eefb61e71fb4066dc56c247904be42015ef755861@5441920c 354e0c970b39c6956fc9660eb7367b61+12062565+E27cdb80616591bf8781d75f2349c12c7261338b6@5441920c b7c109d474fecd5b568fd8e460e81d02+39769591+E66648208f40b52f1822c01c3d61c374b1b656055@5441920c 6b6b334d6fc6ce94572fdcc96dbbb204+15604669+E690739f9742699cd09db1fb6b7c8f864916663d6@5441920c 796452beff88c6c0b46efc4b93f14ee2+4141622+E864cee574b2995464159f65fcb48768275ec1649@5441920c 61b6165606d625f9e2f5d22966e9f6c6+67106664+Ef0ce0c9615bf03becf58b76695fcfbf57596d5d9@5441920c c3c76c86ffbdb4331c4d29705f7bf508+13102561+Ecc8d369181f0836cfc5964c61e1e36945eb163cd@5441920c 40b30d29c63466c5e6239f6be673e456+16343642+E2c9d43453c0772ddd2619efbec822e08dcc33967@5441920c 386dc864e33f0436b915d5fe99e568fe+16664730+E20655b581566fdfcc78c0210d212eebddf4ee191@5441920c 6310b937f32c88e68c99d1065dd562ed+13661616+Ee8f93e9678226b32596883f5283d6271b57cee3d@5441920c 9dfc7371d62085d018c01f6e734d7666+26472421+E6b56e878337cbd25dffd733e1613722630682615@5441920c 2b26b973d557149726460d0c84dce8ee+13161766+Efdf846b7114c9dbe0f464dd7fe5226600d66decd@5441920c 4822d05c1f1061302f5e90ed3e33eb32+26136564+E293c1b58e1dc2eefd8ecd3dd99357b837c2d1165@5441920c 802f5e0e9957fb6fef23f77d1826e5b5+23561374+E05b6f3952dffd11ecf83c61f3eb2dff941cf0d48@5441920c 6f81c5950c9bc67d7bc82566e8735fe5+22349651+Ecf7818e4574828536cf5416cc67b87e5233b1586@5441920c 969c688f7267f6e313e4f0fe1c97d3d4+9400437+E697ddb147c825d4b09d2f56466ed5e61cccb10c7@5441920c 23d33d2b86f5be5e60626f213453696e+6696401+E5476e70ce697f686bb63f6927c758653123c7926@5441920c 01fb6544531e50d5dc982f54c6945839+14463365+E796ffc2fb3492cd5f70b9e46b09e7b904e86c186@5441920c 5e10cce37c60cc768ece04794589b362+2797932+Ec7bc4352c6c25f73fe54b62f671673701b676488@5441920c d27e35b3168f6fe30d6446d469cfb82e+7140760+E8e0e1d27865edf69d6f162f262f418267864b716@5441920c 79ce0bcc5f565e689c44df3b2f299690+7956760+E6b405440347634c4d780d9cd2f751b1b74801821@5441920c 6429667e76cfd6c7049b9f2dc83d2e02+26100130+E1c67439fc75bbe8822c11f6be411228c75474346@5441920c f63d64c68edf1058f8042054d9e608c1+15570132+Ef59753bed1608c150b463db19e0b824c56180472@5441920c 3d76591c1fbc9b1cd43216b53037d3b8+12079936+E3659f239292e2cb4c86885b44c6669507140f5b9@5441920c f438cd1e753312868038166908b7746d+23646496+E2b187c62f3015562691904e717f0b766b1d119f4@5441920c 476b689f6d00f5d5c94d4bf89d2d6f26+7320072+E7e4d35700d55497f8cc8188559d256f046d0bf16@5441920c ccbf6b908e6d39954627695372c66646+10249929+Ec220ef724e48c90b31d0c396802df409203f44e6@5441920c e25bc8599399b2b9c174d0b866633d63+13622024+E736877b72407836e424889479e46e60506db8c6b@5441920c 573e08705ff70f79d328c60c0dfe1151+7329647+E978fffec456ecd2633409ee866f9bb9311d976be@5441920c 5f2772d86c6567de1c03fb9b1535e6b5+25915639+E26e094692d34cd8e6e51f964bb8f147be4825d0e@5441920c 6c5cc886928952bd46f1e0432e966c39+6902437+E22272c74f82664339e62651c6373fcd997684ebd@5441920c d7581c3bf65327e93bf6cb536650063c+19367309+Eb904b6e6c9337464e0bb3e3b1fbcd0bf4228726f@5441920c c4dd8646b372463c3ce23c3604418ebc+10334901+E680573c727b403b3c7d364e9076479e6c68ff635@5441920c 16fe696306debe5906c75fbfb4f35e82+15956391+E68cb974c31829f20f4381d605c396ddd9021502f@5441920c b457f19b1c560665968f580861bb5519+22361464+Ee0e5f7040fe15c3d1138046b4204e2d81ffb09ef@5441920c 6283c883239d206dc8d7bd20439ff2fe+26762910+Eb6598d1d22ec11840f06949940cd671e16f54d66@5441920c fec697c5e865cc4e4587d9e2bf4b1df4+27462517+E6cfee6c054636f17309efc8185cd86cd1d0f2f28@5441920c e369d98390996c5b6d124db79d188615+17696144+Eebb1069fe1f6f406c36e2bdd4ced45961d1f63fb@5441920c 6566d9d439d70e07e6590b7232bc6dc6+16115379+E4615bf36e6691866358c30874be71993dc04c491@5441920c 78ebc34f2f582b1e58e52b36cb9b9fd3+26603399+Eb868f96c8010eb08b8bf48fd6689d884962fe856@5441920c 7b306f84f006e652f346640314e565ef+42767332+E8696fcf20e694d7e3190d2263dd0013486d9e286@5441920c 6164704991bcf25741294e26fb6f1033+22519054+Ed30ed601783b6f824d96968157e0ff69d0199301@5441920c f7942548dd956c6c02c1eedfd2755947+15623994+E8dc622096c66e7e459680b0466c97c08e968247c@5441920c 55496f6870b58c20c61c69e329f86b18+50651137+E4d15666681f614d666ffc6033cb2564fd498b422@5441920c 511635872c2be5d2773ebd578167369b+2340763+Ee6f8d691449ec061efdb7db6e67e686446660060@5441920c 5e4db617b4b314863d3df7f5f7d40b46+12296366+Ec52619bfb7bed7e38283cec6c31c629f3b43609e@5441920c 2c1c155e211f8615f348f56cd4e2eb68+19160541+E4d61dff6db10bcdd89f30333c6f416c4dcb10050@5441920c 26793ecd9d648d83017188676d1e468f+21150112+Ebe666f5f9db78499070dd5cce17f1801f5856395@5441920c 5c19db5f2feb0ec6cc247077326132b7+15934102+Ef2c65268fb7556e5008c1ed147e6cb62fc23b8b6@5441920c 18924490df2fe7c8bf536710b6fd6766+9572247+E7606e9814ef7776e16c3661693f0490c94195225@5441920c 640e94c562bf36ddeb0dd226029eb0b9+37063925+E6ef54638f818d4fe8c3dd65c8f3366c7e5d74607@5441920c 14cd1cc7e24f6f166bb26dcfe4143ed9+26279656+E0e62c48482369497792441dd4672849654fb0616@5441920c 606fb1c0c699c7ccd315576b02e692bf+21312663+E05ee10d5f8cc07fcc3cc665d0efe3d1b297cc615@5441920c b42c6410199f3c4b0e54cbf94ee88980+17966553+E6b79e87c11e7fe96d5d960fd875261711e66f06c@5441920c c286916e594c40952556b7857d67e889+20502272+E1b528c0bb53c020dcd3581d629845bc1c25316d0@5441920c e285ce576d5090b707f24dd699667c27+10454346+E6384d44e091f0b6379d8523d6defc6cb6975eedc@5441920c d530986cfed06e608fecb1191df8c11e+26240932+Ee061638e4f42024ef17e01b02e67383f15c14593@5441920c 3536b5d45d919cb866d1569d96f9e939+11477343+E19f085e4dbd379e83c9856956386bccb26495d6b@5441920c 956fd18076397dd9602e5c01ef76623c+16121702+E976fc641f109ceb585672eb795e964c6b8f2f509@5441920c ef33ee876d98646e6fdcb3867518b6cf+21665969+Ebc9b108234b28642df30c976460016486d27f2c1@5441920c 7426c8c56917966f5e7d867133c104c4+2106601+E63d93fe162433e6744e8bf1f63613d3994d46615@5441920c d12b745d9bb4de069124635140d94e66+22234696+E39d77b9c6db4180d930e44d7e77594b7328cb8e6@5441920c 7cc94de0506c1800b23456081e828694+17466445+E3ee18b1031435b6c714cd132d53324f3ec004ee0@5441920c 1984fed8feecb6697671f6c7629736c6+27353500+E69652dcf6edd66d6f7f223c87526ee683550ebed@5441920c f2ff43078422e101efb31546d513d917+7951115+E08c6d30fce60f953131ee9639397d6b9f361b6f3@5441920c 386b40bcf914276c970f642f66521be1+10132647+E69d7529f917e1874c44c21e3c1d391261690268f@5441920c e09636e09cd7d8c32b6663e95678f4b4+24122390+E115e9506efd385e3c03b51d274b136cc283cdb61@5441920c 5dcf989f58765256e745395de2c16d69+21750606+Efb7615104f94c7b4bf48ed8ec84e6ce1f884632e@5441920c 686e5915cd9858003f6822546d6b6d4d+15546705+Ee8d6933f60c51411f136b86962dd7b30c27f466b@5441920c 01486366fe6d0482971666c98fc70766+30792695+E49c08c45d856d386f968485e4505e36fc823ec2e@5441920c bed62e9e6bb42eb6006f7065e6990e18+17604912+E9cb886387c324b05c6be038882bd29434cc49e7c@5441920c e2859d677d7c237974c872784e13e6d3+5164960+Ec46db70c565633fc267dd6d133be6bb5891b6c4c@5441920c e757577865ddb690336d4cecc386c3e6+17296739+E7d1dc238f71762ccf46766627eb215be08b3d5b3@5441920c e2d2f7dd057617592cf9e4317535b11e+3301773+Ecd6413cb8c4e5b795dcc5680693d623b91744107@5441920c 0622825df321b6b36886ce672217feb9+3676756+E46d666c70e222477b3337606dc209e5f6cde7625@5441920c 23566113cc3c2891f84df717b6b5ceb5+13263209+E6f1d139cd24c47f6b5bdbe5636d49d2140745175@5441920c dee0ecf366b0e469126252646cd78667+5712724+E67405ebe84168df10534466699ff60c899055389@5441920c ebe97bb12f1656d3176bb8ce05dcf62d+10516666+E79769813ddcc30681b29180676df6666c06b5164@5441920c e16b6468eb876f7582d666b4538796d4+10144603+E6681f4fef94f71787c6bdf60f73ffc31dcecc444@5441920c 908963806d665f6692de6131cb688e3d+15620599+E19d63801835710d6fe726dfd3002226d59d1e6c2@5441920c e55d7c07c1351d8d52db166be6b8c09b+26940326+Ef7468138dee02cf621b8869c9b9e50476fec05bd@5441920c d4c83966648b99e8dcf6c6494d8d763c+6160746+E24c413dd9f4c938f3234023714c5dde6dde24d2e@5441920c 530f167ef4dc42e18714b4d6fc79bed3+11144267+Ebb553f3bd952cf396b654261465b55bbcf814826@5441920c 16c2c511d3066e6032f465bee26cb26c+1431977+Eb914580870bb2b6bd01dfdb14bd83331470484f2@5441920c 0b71c2164058821f852fc4456876c7b6+2244756+E6c07eef683e14f34fe3f7f066e33c3333304e6d3@5441920c e65eb96f4c91605b01158d56374663dc+9266561+E0b5bc104933b16464fb9d3f15555b6eb321ff820@5441920c bf69063c2fc34526666771506f68bf5d+41245659+Eed1f87b918f56236ecff73bde704699ef23d9fe2@5441920c 6056068b9e9989c1c3260c3501865930+15344510+E04e7df1e225c11c83512b4029fbd2c018b256c45@5441920c 3e5b8e59d577b16b6e84786242521806+24932791+E74c4d89582d84340cdf5465fec29706076667669@5441920c 634d6b1d6146338e38344547047643bd+22442446+Ed776175e050fd858036380649d6482d49287d096@5441920c fb5b4283359e7e5e366c3606cd8894b6+7752724+E680f4054419d6fbe710970d65d33bcc466613cec@5441920c 9b2911bb7fd67f6cd4f64337664d831f+26224360+E20cf4b6c243f160fd6f86253cc6377cdf46873d6@5441920c 63f6692b0fcbe33870031f8687547dbb+17304639+E92ec56f25f729945fb30562bef77f6684645658b@5441920c d96d6835e084f2c1eff67c52f566f6cf+16113075+E454fc1c125573183c69bd5e5cbe26f4bd4412670@5441920c 1d2eb0963b1fdfc11f6ff534162728d6+22233411+E60b2eb26e8d067f3d7612bd3cd6fffc46de1fdd9@5441920c 694b1b84ebddbe61749d6c7744e2e2f3+5524922+Ed932398d61660693e39554b50b2212f8d4960971@5441920c 0b42c92d97c0877b04d33666f22509c5+9664262+E9ce27760d3e7e05b965366f712b5e5f349638f54@5441920c 2cfe498b5b41ff5586b3c18fbf175d68+9160746+Ed2db55d98c2efbef816f30972eefb7f366705618@5441920c 6607b727ee38d0151e22927e8432e2f3+7956752+E20e43f0628df779e08c742dc2651861e4644b161@5441920c d782d2966fe60ceb61760be1891ee909+5100756+E35634fe29d03c35d26f2dc6c03f60272f0674160@5441920c 606030c626d5e94c4062618c3f652b38+9937902+E5662749f1f2e19249023941e760f73fd6df66334@5441920c d62d663092ce6f4d50361f36c0232049+19546232+E6316b6c1b16bc310863d18e7e387e35e4e001d27@5441920c 748d7e6865bd463de20915f53be86056+6663394+E7609edc173c34c9e36112f163563762933d1d284@5441920c cd0b2e572966bee981f066b967c25558+11752445+E224d2e284600166f66b0dd65562f01e7f6bb495d@5441920c ce1806850f36d94b326506b6d9763515+19256022+Eb4b964d8cf18b298376b5b42e1745c925fb6b568@5441920c 3496386d8279d2519c237764d914f862+12954653+E94e21b2f6c32195f270ec96567f6135e4c9d9f7f@5441920c 2eb661f4b584753660883614c14650e2+23233415+E22c6fb1b5e3d821378772485590ccf287b46153c@5441920c b9465b26065de0b6fb1fe66660060fc3+10667296+E95b5997d369dc93fc0bb8646217870cd50110f4b@5441920c 70485d53084d944674663bbe07336639+16966664+E5438d9ef89f2512b426cf230e9ed03461e490566@5441920c d29c2195fe6226f7cbc596264b1ecb9f+31566677+E9107c836bd1436d9d8f06e0fe58f74c36619eec8@5441920c 170d605b14e135f717cded781b3659c4+26966370+E2e596e0187c64ccdcdbb7ee379d59139eb84fe7b@5441920c 0836cfb1122130ecf01d5d2bf06def42+10993650+E4076b5402f7e21f1639dd22370286d76b7c67565@5441920c 851e496848b92598c551313836610426+33045521+Edb0688f6969e275c687f66bfc3be0318e674b13f@5441920c 6e4834f41842034f423626cbc8c2684f+14150927+Ec802066b016ec6ce60661b2b46c10e1663b0915d@5441920c 90e67d4209cbf05e9df318e63f645b54+23576635+E3b614e8f064e641468dc25c8340edf12d10cfe33@5441920c d933cbcedefd194be06338cd661d666b+14665552+Ebeb1c374bd74c56625c164d2e9660134d3623069@5441920c 76922bc107807f84b39fe3c763def0d0+10410131+E62f638476bffb582fd56696696ffddc344d55417@5441920c 7f523f4e5b7fe74b758f084c68f4cc3d+14156634+Eeeebe9b69ffb2424771669467f0c53ee18323294@5441920c bff64b2d1466d691b7967513cbd13dfb+7576172+E95bb1660e199d76d1fcc3bf756844d334bcb5ffb@5441920c 5346366f8e228259192b1fd25fb03174+44109465+E0cc4fc81f3e00e2626ecd5990388e38de4758611@5441920c 23c9d6b46c17b44f615072641d7f1ce4+31254935+E3b51142315f65f48b8e9cf29299712b55469bd9d@5441920c 18c9077006641766e2b0fe36b698011b+5169067+E083cf08445fb6c789417280f85938d6dff9ce4c3@5441920c db7d18f27edc3c6ddfee633731c2be53+10366921+E08619bee42652e512c63090e464049ec58f5502f@5441920c 6407825d48318f5626e310333b4b968e+29052271+Ec14b855cc4525b3d31542216d7b03c74d301068f@5441920c b96e133b4557374be68806e19132e43f+17612627+E775c6d28d896678b02f419e1215405db04db1dbe@5441920c 61f26b19764e01221b6f589b46f2589f+7641759+E4ebf4463513e7ec8326e77eef8b443b7deb6fd30@5441920c 580506ccc464195d925e3bd2d37c2b89+13411716+E534f2d36ec36702153d2ffebc88bde6d16681314@5441920c 0ff594047bfc075755fb6f6d368d4446+17757245+E686ff1689039794f81c901ddceb5bf96b002f471@5441920c ce16005db1fdd8fb664184e4897ed848+26954567+Eb1925567366e0b994d07507992215736ef795c6d@5441920c 668b8606e229bf34b265b5889cc2555e+23246223+Ef8b4096de374b658d17c366314fb2b78d604055d@5441920c 606b63de08276f2fe96e97839f80c725+19074161+E8864426fc98765090568d788fb706bf66eb2025d@5441920c 6f8436bd2f31836f58cec1bb0ee05636+39449695+Ee8933dbb69c24d6d37c456817370dc907b5c95ce@5441920c ec05d19ce5eb8f336eb13e2557d63124+11696577+E964d26d1560036f27bf8776572362c03e4e9f7f6@5441920c 44385cb347d456b6c56885e8de160e6f+13249663+E46f5be980854e6bfc58c55668db28c4090cc627e@5441920c 706fc99f3855ffb94b6d81c5c62b069d+6706592+E1053643769f364b1dc5f766789b747f0bd383d4f@5441920c 6652574701024f5e2f18edc6fd036681+29162964+Ee1b80f61e7971c1f4b3e55260337cd33266c0f00@5441920c 14763654521f8d4f6bc427d3b52e1121+10264945+E69bf6eb86179653066b92c30d288d34e698fc996@5441920c 8d6f406ec665e80c3986fb2d0dc39ce9+24601643+Ec7326d895814e06dcd041c099e25f26cd4e3c214@5441920c cb32b5ed637e57de216603c266184249+5951761+E16690fdf9283ebfd35e13f67307432b23448fe63@5441920c 565e8136fe7ed38f038b4236b645375e+23795506+Ed68ece3056f828c9e5d7fde3f240c404d1c472f3@5441920c 710dd03c65252f2187e76e7666d7d120+3150007+E19fc4021fdd7040bef16d9760fcb94c312665319@5441920c 9bfdf090c2776f258c39e549391c1612+23077469+E7637186506886b0e54b1e3e32cc578ec49c6f3b0@5441920c 7792c138d461748e9e5627e9b0c76c09+25966072+E82c28076635440613cd55d6993fecb03580f4cb6@5441920c e8f20b6349d80e1107ebee500169b8c4+20640325+Eefd81f4895e4ed54edb560bc5586d8df43456678@5441920c 4b61f2ce70111127b33f6249665c3d47+31996632+E388eb8561b2724c225c873e14421df59ecf09fc6@5441920c 661fbce63c16de3520d44e033134c6bb+43632512+E6ebc3f333f55fd0f69c7c5bee2687d1228933e8c@5441920c 5e6df4d8cbb16dd620f373369bc8c9d8+13731959+E16052995d0bc657b3759b020207ff0e3e41369b2@5441920c 24f521b431f2d770f1338700bc6c6917+12656172+E6e287eb610cc483fec0218d27632b69c546d01d0@5441920c ce0c0138e32619861616966d61be5915+34247127+Eebe1b981633c8728f6c69815fd3b88678266ee96@5441920c d870092429833d18585b6f4ec01dc640+13266016+E2d1df1258701d32d0d01f7613b88cb196f410262@5441920c d0728b8d923894b266361b90f06c047d+13161256+E8f4ec81d6944833c3dbc3cb1de240cdcd5f32ddc@5441920c 4c434f76f60d949088278498e5512652+29663052+E7bde3f3f63c9ddd47d466442e2c7116bdd26c9ce@5441920c 953b273715e8c9c1e89155300fc76183+30634366+Ecf3b53ed7d6bf5fb62e0861446615623596fe359@5441920c 432cd2317e2e713880e73660431c3648+6075493+E4b8e179f8c779951f5697e454dedc3b2d5dc0498@5441920c 3251f5bb90f3e37db864b31661297db2+21661204+E69bb7d26df5775651fc098e530697067ef65e343@5441920c 1c6168d1547d01601c45dc5485d6c8d0+33606107+Edf5663b3b04e139dd68bbe6960d6761d14e7d96f@5441920c 0482dc465426bd763b346e8474e3306e+32791910+E716d0c2d56e2b3ebe74922786f5526cd85650c05@5441920c cff6ce7521e98176cd89b1996cc6b2b4+19669112+E33760661c08ff206847f837f6629f2246eb24f90@5441920c 66b606bf24b864bc3e165cdec26cb2b6+4741605+E4175e96fec0c423932d88eb21ec0c63f077e9683@5441920c 63c8d322dc1998841b3b5461070969b5+25904705+Ebce6d95c6e12c00ed08efe4de69856ff194fbeb7@5441920c 4c672b0f4d22e5b6eb0fce791981b5c4+23619321+Ef803e6ce60d662931e60c1e7eb0e0307cf719639@5441920c 212c8b6286520d28024171b6316919f0+25423194+E15c722ef20ee1217c4d25e7be6382e10887c474c@5441920c e9477cfd645d0e20e85ddfbc827b65e2+9119290+Ebf9394269e1967cb3206c86d19f7806bbe48786c@5441920c 9cff6d241881d3fce55ed434be59b717+30796914+E7e404f9edddd288648e0b656bcc8c651e42eff34@5441920c 80666518360e529e9d92c201ff716b4b+19924674+Eb76685753d3cf6e3f358c407e48f038d7c351613@5441920c bef263464b2c50b53b2f6869099c0461+11135309+E6951358e80152e40712796312f6643ee63017b3e@5441920c fe8d5b36b6408eb475c4e1e446649058+10940177+Eb66e396c194c25e7e9fe16f92e25521db80e4036@5441920c 68d36b133c7dc81f5934d70c0617575c+23560116+E2cf474592b605bf861651e09cff2f62e166b351b@5441920c 6332b32b6617659b3840e701167c8222+14661122+E5627d019f688cddeb6b8960629d88973232dddff@5441920c 4e4e6b6b40d792e7c70362b47c968b2e+22609615+E4db8cb76e66db83f0806649bc2101046d4e6b254@5441920c 94668c2206155f7949de7c58f61404b7+15046616+E0f9495622572b5ff0cfc988436bbdc76b6bec4f9@5441920c d69777eeec1b5cb599d6614629b5142e+22166262+E27b789796f685c194502ce65578e5b7149f65b1c@5441920c 95edb266c0f8249c9fd662bb2b15454b+2056060+E8e86762f71e0637c7ff02fe61ce0e4e75f07286c@5441920c 6262cd49c44b278f3dc3c26626811601+521252+E23d8b5757618266f67ce07c658f9b85832c2d162@5441920c 00d93614557937f44de6f05f32c52790+32234144+E0f9f4506067b8b03b2c341f52315fef5bdc7848f@5441920c 3e815000b466f885958480d629962711+6441932+E9736c4dcd0db18be05035b497394832e06c46263@5441920c bde54d38f7b66830c212f9fd8215dcbb+10946699+E1be619426673d73606614825b965d48c72335766@5441920c 6240c463730c510e1d4c78899ce6d1fb+21772696+E480d296eff2c33873c7636eb723e8731c6959ce2@5441920c 2643b11d023f4f0f7614363664ed94e7+27069700+E0634917365618b24d91c37c3297140430ccb2556@5441920c b8cc10de6847ec2063bdb661f54906ed+6545313+E4490384364cc607681f6977b50863dedc73607c6@5441920c e20c9288b641899dbf4cebfb56ccd9c8+31767795+E44cc68804efc112c269c26c4369c67ee6c3193c9@5441920c 3b20e469d187234df0465b86666599df+23275612+Ee611c6930c20756cb200b2cde45057c242457cb1@5441920c 255f4954bcdfcbd1d4e0defd92985d29+29739564+E63b62f61cbe87116d56397b35bcdc3b91114db38@5441920c 5e64c3dc3fbb136403f47c18b06d9cfd+20035093+Eddc50bf137ee36fbd1f1fe0546533e97536926d5@5441920c 600fc94c672bde2472e2447effc449ee+20162106+E47b5116de964dff76b87bdefd81e56666f020f98@5441920c 057e9f3925259bf349d351cd75e01146+3767564+Ec46b4b19673ec8e14b697f77fb76c9822e51b100@5441920c 6d3c76220e433618c237ef6dbee0b20e+13561503+Eb1d89b38199393599fc613ed23e359eecf5880e6@5441920c d227c3f6355fedec0507675b7103e86d+7002557+E461eb64220cc189b14f845c1c696e7020c63e1fc@5441920c dcd0ed86431d171f935c2f86d6102166+23576165+E381238e16b036dc6df7614b6ed2f87c4324c8fd7@5441920c 2666135948e62048f269cf807ee5039d+6615671+E724406f1702c8b9d4b54865b7b197de08e68d057@5441920c 0bf0328bf9c3e9ccc6c386febe043cb7+24662143+E11355cc83b85db6d6d743f608c9db4e3777378e7@5441920c 186647736b93302dd61610f424c8b366+4697534+E40386c60866141c75cc84c3ec39772920b7e0196@5441920c e4ce7d1409ee024c1646e2bd9116d96f+9636940+E88d6ce1c9b866c666e86336e9d406b678efe9802@5441920c 463bb99ff72e344514e00683836dd4c4+16496116+E16301e6dc3b6d434214ebe82fed7333d2d201661@5441920c 3c8e07176c59686e683938069424ff92+9016631+Ef746d69e963c8380dc3b93f3cece96f202893f9f@5441920c e35874b80f896ef662c8ef10509d8612+17929166+E60132dcbf4fc5851d3c387420d13255457dfe9cb@5441920c 0f6973694123d553842f312ee1e7f9e0+11594711+E6ec26bffb5c736d335bd77d57d8c8e2f1be866dd@5441920c 9000642526f472c77960bdf873fd01c1+20306666+E8d23013d5065109c13fffdf723bd66fbb608b949@5441920c 7b237e32669f8636b96ddc6bd4bf63e7+9030401+E3c2be036b14dc4d7ebbb5e86e08eb8075cccb8e5@5441920c 36b36df49e07763417f98881786d8559+14696627+E339168621266131471b452731dc9f62f73bf056d@5441920c dc5694ddff5394218744de5398156668+10006611+Efef3e681202d6b2c713e659ecdd26df28e73867b@5441920c 06f25d6f4944c65bff12b41f65b9ef6e+15979710+Ef66d40d2473b250e733bce068b416ce526cb3f53@5441920c 3e333726fb6849b943b4484c1861599b+10116166+E3e7426053bff164299d4d397165b0fd6220b8dec@5441920c 12de047e2ccc029d5e058db8fc27468c+17606797+Ee906d267962f6f0836c36e376deec2676189060b@5441920c 14beb6bccd174c0d46c298320e76bdcb+23227207+E234249e7c4f2bc3e62fd6f6b0443f0cc543f1147@5441920c d18bb654f006323febde3039f09553bf+10556009+E8817cf8fe57546d649f5d4505d43f7e3f179e3fc@5441920c 83f045513b4f3f4021f65e845bb6ddb5+15162933+Ec1dd1156d1363c11f887cfc8ed001bf86835b2c5@5441920c 648336f5f4936526b645d64725621826+21307590+E6de931671f89b44940b760352e2d0ee530442267@5441920c 08f67de336c072c6d662b6cf2c583861+32759712+E8d66d86d62c0cb53f7b7b45d2f5b6e363622e2f1@5441920c c80193521c5914f978569b404b5d641c+16676434+Ee05b6346211c4f606521ce55234127eb763d5bf9@5441920c 436322cbbf425f6618b10c6d369ede1f+15100163+E60064882f33e83e1c12473c20957cf176fb53e73@5441920c 0bc877878b00f4dc96e0b93be374d5c7+10434017+E1e5016dfe71330e966d163b325c738b126533ff6@5441920c e6b152369e78d29bf8251e0995bcbc17+7765476+E2e56fe08f21e08d1693903bd29722e3f679bb7e2@5441920c 4e6f2124dd087175cf26b53d70f2f816+10542610+E481819516d33f267c0811e007365904f907644e1@5441920c 61664890644031d2e269f642f2669466+6767004+E3ccc64d7245c916b473b8065418527f5def1209d@5441920c 358cf83e673e0b5c9b9ef4316367b369+37253134+Ed1329475fd3b9680fef7648dc3cd0b086b4fdbf7@5441920c 189308e56578d296d153b6d7cfc8ffd6+17163652+Ef2342c38299bd6c8033cd9066d94f9f0966216cc@5441920c 480cc0e63c7d3c15621bd664fd67b509+25746921+E0c75c64e567f7cc675633894b80d2c1f55534c9f@5441920c 6967c4f89e8b866d18e8015d7786fe54+17296262+E6ccb6666326b056be3b82ce978b61656c55e81b8@5441920c cb619e0608d05d333ee66dd60133c26b+19044617+E582576631693e5f9f6463c79285c6399f10dfebd@5441920c 5dbcff65061e1859129467d669376e57+9216326+E66b33696790ee67567724fd77bb6f081ceb0b1b6@5441920c b5cd4c03fd56ce74f47534305f41966d+7709647+E369d45c56d9120b7f54c926cd465b6508c061168@5441920c 021c56603b593fe7b0b9341d3e69dbd5+9992471+E5904c358669ebf85d9672d96b1f05562be4cc1fd@5441920c 25e6d8e21638046d71fdd9236b5c3bbc+16105743+E0b235dd4d6ecf49ee503194de09e83995bbb8b37@5441920c 1035df4f548660343340651661d54861+23723049+E860501658363d21944c46d2861dcf27375cdbe2e@5441920c 66c958453bc3c0536053228807554242+26740659+E6bf768f4c64b76980e71f3986f653dc17dff3fb4@5441920c ee27ffe36861e4d610769c1ef36e81bf+39101465+E07f66cf945d7636bbcf48500ed84f6fede43066c@5441920c 366f8f5b1b4ecd36c3d4bc6e28454223+13179037+E0ed961bc4edefbdd47c9659b746fc485361b3866@5441920c 4545c0483335c548e5454620e8087531+23659026+E16019b3165d481b46fedf5506606dce182507e93@5441920c 27df881c9c906dc3fd04c2ff68d7f69b+6320674+E6e2799fed96fb6f5f8ebf32e18680b691d9528df@5441920c 28b9b320074163fc02b034e862246754+22624620+Ec26668860c2e9b956b8bdb06b69536b65f34d974@5441920c 5968de9783406e3cf1585824f3068095+19209706+Edc1ec66fc64dce19c967735840c19791e0c7b9d1@5441920c dd6b0299fe83e269b456540618bd4837+6364513+Ee18b3f6bfdc9e1ff8f927e61e1d3c9990bf259e4@5441920c 56cfe9b7296366840fbf3c9d0cb2bdb9+4766253+E5296479852716963f5749f7866dd919322e284c8@5441920c 368b66c5265db5c5613316566f7f9652+35016116+E8d446e66889974311f73c0b7cd682ed4dc4163be@5441920c cddb3f3bbf9d3c545fd68d76983b45bf+36549974+E6f26c14bf26fc4c57903f698475076cbdcdf1f07@5441920c 7cef19426bed80ff83f9d900c8178667+20373460+E5c6cb47d34cc6495fd887903d6d6666d9505d761@5441920c 8f32865c19f724438e2d9b648c6640ee+29919661+E16c07cb08b67feb34fb9c76b36206644f898976e@5441920c 09043bdd449b7946c4fec913e4217364+13493460+Eeefc5c2c41ceb676feb866380ef68062579196c6@5441920c b5d1be0d12754de15166479f927dd02f+16466490+E28ec14599eb0db52480483f68be739f4ec638686@5441920c f483fb8e54f6e1763799e9df42f08950+6660416+E962653dc7f63c1f1fbb6856633f1c2b857de4cf1@5441920c d4dbcb0d851764f4f94e4d62996d7261+7796021+E91f5483255146fb4f1eb66c2b797b6e924b8b108@5441920c 6691b09543c044060441936ee10221f5+14575657+E09fb9c6678805f0e7b29e290177f1d2f3916f0f8@5441920c b19b9c3869e39becd78c8053ff63c6fe+5634479+Ee35b3b397624685016de4586c9d96f57fec9fb4e@5441920c 94208330d58de63b7b603355845e2e9f+29716269+E0d811ef3d936670d06e74b9fe6fec5c86ed5004c@5441920c f024f736cff9618312339bd9847f08f0+7363995+E4b972de47eeff1038c3be68b28c652c6750cf1c1@5441920c 8b66166d236682687322174e707f5bf1+19715177+Effe9b54cf6b0b4156cc78334b71ed29cefe2fbce@5441920c 687925fb3f001f6eb17e262f7f3bf6c9+11922350+E6e45db64c158b168d9866928667ed8c5e400dc37@5441920c 811bc86929c6cbe690e68e712f81df76+34696356+E1d9dbd19b5b1f6d16697890d4136646e0b250567@5441920c f88343667b26669cebdb91160bde17e6+33645974+Ef0e4ccb520cdd1fd51f4008b596e370b8920fc63@5441920c b9e2c0b204645f0b5ee13776052de068+35567370+E78b5f0cc1d71b91ee13763613c715b5c0d946874@5441920c 516603449b0e68dbe1f10916626c66cf+15611642+E35b47868610850f2b866fbc566936872708ec8d0@5441920c 5ee193db01448f87063d7b854d07986c+27146461+E8c3b7df08f26c457d654c4d90c956b75d3856660@5441920c 928de1604d0f709c62e23fb2f6c1d3f6+26736354+Ec954bcb4e6951f3fd82e89f4d675d0d6655b5ff5@5441920c 17c99db9e4d850c53408ff6593bf4e6d+12053649+E6418566222886b6e5003c6804f92327c66059e3e@5441920c b28d67d5c60e0165d639695ccce06c60+45621670+E39f873b0cc620266d04f5b32482c68e3ff3fc15c@5441920c f7d21c881b4ecdc6105befd96983d442+10457142+Eb98210f27687e94d9f8921502c56bfd5b0606e8f@5441920c 0bf6beeb6097903930dbfb6f397363f7+27163032+Ec0d77f7bc0182f422df918f597e01f9e6b7715be@5441920c e7170d8075f74f96bb230515214907c2+6901657+E9e0b89feb40e2267f5d94df2d1993ec640268b53@5441920c 0e59c8753f30cc7cc9fd19f8e11dc5f6+13650247+E0b663b68366d8df921269fb04bb7f72770352066@5441920c 29f864d900551cf85dc33c850f49061f+23602906+Ed6f0120d02dd26216c5510c1e46bd109bebf6681@5441920c c8f4528bd47bddb5b26e4006d9cc89f0+33300672+Ef1b055439022dcb8e5b60721226028cf60b9660e@5441920c b84644525493d6b827f166d0edb616de+14622270+E56972e6be0dfd68fc0362332ce43cdc55f9c30be@5441920c 04f6c936ef65edd854c2105b246c7d0b+20760162+E8410cc9b133b6082efe33c6f42996d304708984d@5441920c ce1c666fc933026cfcf39c0221987462+29757577+E8b297663f7f1b63191103790dd7060374535f380@5441920c d6369df6628f2d7c48ecc5726e544004+9439391+E4d6b43d61290d3383d50668b68ed1b1d3b86cb9e@5441920c 3092d05f3b8f55ec765c8c95b6b40622+23690991+E269654b67d14c41bdd9920303500003f0e930cd8@5441920c 50b159ef9c1213116d947c92285d4983+6504376+E8e941656effc51485f2f6419e6f76d6b0619cd65@5441920c c8543b693d01c8fe6cb3728ddcdbdb22+31429424+E96e17b3cf08bb6494f841556d6037b6df5cb4842@5441920c 4846c99e6bb5b179de4fd46edf46e31b+20667266+E89858f30656fe456b6b4c2271fb1f5fd98b4e9dd@5441920c df48275087655f67867539949f52cc01+21542259+E6d2796f768f4621eb6b7b74c3322d1bd2b3d981c@5441920c e07398c19366fc4f876b23bf79049f1f+13635330+E24336044e23d35569f51f466c47b1c0e3090666d@5441920c d27072861c18d3d61663bb359e61d1e4+16645443+E57c58c03f56666fded7e95596e6017f6458f5e8e@5441920c 868b525fc41c185415fee9ede35c9b7f+33763672+Ed656e64e7f08cd2363648f29446f84f0013f3662@5441920c 634d4d1116c85199b4c8837667126628+43935944+E51dc654d2602ee26618e60c8842112225ec2bf48@5441920c 6e8cec6b84b340b746f63f7368339430+26173344+E7b16866c76fd11f50f6768172453cbe3c83385b6@5441920c 1c7746f9733e0ece7923ed3537dd2966+17960379+E2c7f5850549c1662d20c09e330fb173c243f4f47@5441920c 331557d6b124e16eb4de307655c40882+23669100+Ed8c6c496bef0c4fd0866922cf4d7762b2b9390e6@5441920c e200e83c9304eec0022b7521c3d8f256+21475297+Ee99dd49f41fec9566f6fb75301236673737db243@5441920c 9c76d58e16d65c05325d12318189b06d+16293653+E862e595b7100806e3036dd94df563646226bd766@5441920c 515e08e4b23d320644267cb4946d5e3e+1514377+E67d27dec368dd978e2fc48ee90f59711e5dbc2e0@5441920c 1d943f36d01f35f0f1bf9663b506e924+6509364+E59408bb9f6fd9f3c537000dd213e628f656ef976@5441920c 6d234083cee3e2efe81455d863cc5dc4+43017690+E6b1e7c6b5c44860e1fecbf135b7e9f662801cce6@5441920c d1922cc8dc266b6f6eb0c95cd8b2f417+20665117+E962b126b99c4c8cc216450937bc8c1dbd8e2d2dc@5441920c c3f2e5d04b3346545c2584cbcc9969f2+1591467+E6d7e5836b23ec1ff6336d62f6037e9d3cb92693d@5441920c 867de2fc66953e25bf15c61378ebb781+16146759+Ed18d2b753e63678546bce0bb1196b4ccc23207e6@5441920c d8c6f838e3d60b0f1f7dd7e9bc896cc2+6656200+E2db7f97705c6c32e14562c2776776bc80fc97d63@5441920c 0cdb1913f6fb98e1680d101dec9c07cc+20707621+E9178d6b652d68bc7f61dbbfc942673d523c7d86e@5441920c 413e0b5537cb3d5ce03f9e9cec4f62c6+9656450+E7f00c2344edbd7683c37d786c0c7cdb9168d1cec@5441920c f8083c6ec29669d7ee607223e3ed584d+16425621+E8083b2db35f09487c86c03c0165716144f68112d@5441920c d57b00fff01f31e839921b4109151f30+23196332+E663e94940799968e43e632cb56d75fee8b418677@5441920c c9d80bdb4b75c42ef1154bc13e11021e+7300691+E8681d5461b3d984ed09eed8fb41917b9e7bcbe17@5441920c e4d78db5894943cd403b6b3147c7321f+49692537+E12239bf4e933dfb24292001dcdb3b074969ded00@5441920c 4902623e0f182b4f31fbbf6c1b917ec6+30721960+Ec98bb316ded2bdc765967b66218227f45e4ecdcf@5441920c 5f0238db354266526666793b0b228312+23666340+E0b679d71662d6387682430b11bbbc47737356e93@5441920c 8c0db777b2b8b95785766bc1b47733b6+9229611+E5869e889bf157f2612f20b6d765bdee03476e9c0@5441920c b53c6b59d74ec6dd58b56152519274e5+27421753+E42b4c33532ff2638983b21548b50f8d77b40cef9@5441920c 65bf786bf68c762e3fe62c2357896c7d+9699436+E8f2b22716ef79f09748948eec2c610118f7576eb@5441920c 894c39bec02f51f622e4b1bf202be8cd+6406659+E6b56fe4f277d784ce1d3d3c279763690f19d576b@5441920c 32dbd624968d15ccf65b3d26bcf3e0bb+16694996+E38435585ce3658c50de109653661fd661968fd2e@5441920c 5218162dc863d88d27c8088b7fe0db3e+11026527+E70b1e8d389f0b1b1e635bd5f0219635976f53586@5441920c 2664546541d516425ef812bd00e4e549+32909679+E56d0d0f602d8d2e240dc6ccc4d69d9353030ce9b@5441920c 24400617c269c4ddc9bef64256865245+30963436+E7eb3c301800f63d66ed0755b1858ebf488464166@5441920c 80e8650bf6f2101d6526e85cbf1669c1+17266919+Ec9e10be5668d905fcc4fed6e5856281c4e2d64b5@5441920c 659e40465fc1d4d93b9596d6902258b6+26996009+Ee6ebb57415fd1b8b668e276f34f9b5b891d3f526@5441920c 940b65f21799e622371662b8c543f280+16704607+E2eb498f04302367895cb3ec665eb7941bc62dd82@5441920c e5bdddf3051f3e66608008750c46f2d7+26045175+E6661b5d76f3253e1044d6b266174b6d27fd7b65b@5441920c c4810116de72ffbf10295ed9c07e7685+27916575+E6ef1302d90884fbf836712f1fd74d61f612f536e@5441920c 64fc98e6841b185fc0d82fb136b663f1+15050054+Ef795fd6e80365f9ff17767c6231327463433e9bd@5441920c 6456c7ef22d529c812b5622668f1f84d+15603577+E5577c12c3563684bb5600b4e9be014dec6b06c33@5441920c 687405d9d700bc374b30029cf8d4be59+27716393+Eb0b3748363bb867ddd6dc8c3c8c08105741864d4@5441920c d284d714348344645242506366129f16+22019757+Eee039cdcd2e2630126ed862ff4e697bb1b93637d@5441920c 69b48566c2663ef36086d9db2f990136+45797643+E1c80842642f746545dd1405229f35c3b3dc6b19d@5441920c e32e44b17e16fc2e2113466ff867e26c+22514360+E56d768865cbdc8b4c3c56965ed282e1fee305906@5441920c c8b2896f824744f6569b88fefdd216ce+19253951+E322903880e688b62d3bc146765c5c1750e43f45f@5441920c eb2576903cced0150e92eb028603f228+21229495+Ec266378d59606199c6e5294f1d400b196904859f@5441920c c9d128d476e5c463452d08d4ce0efe6f+17559372+E465f5926d3711b9b1dc8266666fb7ced402c9c78@5441920c 565cd1f686914644b63dfeb72e9d041f+25526673+E49e77665901ddf4f98fb5d61de73edd66b43fdcb@5441920c f293c29e91b111e9330209f3d94dec55+7096070+E3ef2322ce8517189616069206c266c66c16ce39c@5441920c c243edbd633d9795c9008457e7f64c24+23411651+E0fd1066e77be25015675fbf8e338364bd404d16e@5441920c 66959d9139f6de12ec00f9dd486fb30c+26119054+E04bd73dc60b645f68239df27e8707d342cc5be4f@5441920c 6b633b3567fcf12293e447f2f535f68e+23290349+E8e76c6686bbc756c2b966ed43e9fe1dd4f9bbbc8@5441920c 8c6b77dd767ff6e4e4bcb5645db9c267+11654057+Ec5e46e815801c11f9d0b13200539d5b8c05c6b90@5441920c 3377c8e76b7eb9f04d30063e468fe4e1+6496414+Eecd1131c353c78259036e2c36205d71e695ef6b5@5441920c 4026556790fdb1739541e13e97c58e9d+7220726+E01078c064ef4477876ee0d730ccb97c695f72d9b@5441920c 265fc5b76cbd9cbb3fb0ce49dd4ee2b6+15666346+E831444667901b15497b4b1850fb5df76f5098681@5441920c d6f158bdeec1c3cbe0df75466d5e0691+20565771+E66517714fb6121c25260c6d766080d107136b199@5441920c c99404c36f55be9285d6ce7f6c398728+29696076+E46bde61dd962c7659b6bf58c5f24f3c4b0295fc4@5441920c 3162d76defe7c44544707f52d67b4770+29661960+E88d05cc566b526ef6cb76626bd386ed468eccddf@5441920c ebcf6967d9e4c232e2876786f86e3cd8+5667660+E21dd3f48b8116e7824b2fd342e7d1300663ee33f@5441920c 7507d4647d3bf526be5e64f86fc24740+21602934+E6729c2617d186d5d1f828bf0126c67ce3c67534e@5441920c 64745b39622be74bc8c6bf8d566f7f64+6125690+E9e94137358c821d701932363b415c35511b41009@5441920c 8ed366614cf657d3d654f181b698d28d+345265+Ef9f24b9b4b39e13c0859033f00ded590e89e9eb5@5441920c 10b4f10368dd17556465f21dc66c9d62+10003929+E4b200cf2ee068279d431940f687d300e4741c76d@5441920c 748847d8c44cc3c6ec068161d13d8269+6997133+E117d6fb869e6138b8c9cef842f2dc2f60b9b8cb3@5441920c 332d07334b64462499c6fd664e3ce8e4+550060+Ef1062eb63d03656f3368fb088c6e152667662c00@5441920c 386e6796b1756f14b906d6496667666b+35514556+E398d66e1be91928eb0f6725e40096f34c1566bc8@5441920c f699169634840b6c5c22032f04662885+30770003+E23066b0c9607b5c0c848716251bccfff57f57644@5441920c e3328568c69cb7833368c53660bf7778+41661599+E934b4b27473595d61ee61381b912dd1b15f69b3e@5441920c b68239278c162b35f59dd11b26c7bedb+5032660+E13f6f6039e1954d72bcfe87cf714144b71f3c9ec@5441920c 63b57755c3cd8069e5e2626dee6c93b7+15730167+Ee3c7d8676fc6461df3dd9b78c30931b29c569485@5441920c 18dc31ccb81062638626639d1c7bdf60+26696961+E61e3bf0e745b644443647e287252e84061f20838@5441920c 37ff6375ed894106e4365c5c6416d067+33670066+E6d626346726f50f7f186c6602f6cb1166dd7506f@5441920c 11365f54371b62654524fc65e34ee36f+5763371+E315283978f461c40e5641556f19e630c2256046b@5441920c d830c420616b1e0ceecd1361e07575fc+15201350+E8260833cce68c73846008c810d7e910821f6fffd@5441920c 93f16b759b1cb023344dcf15cc7b199c+26316506+E9fe3d33fc9c66091ddfd6eedf752442bb988254f@5441920c c23706e6fed66ded615945667300d388+47367411+E8b073cc5777d624909c6bd3e65b61ed303d5423e@5441920c e24b474d105f11b6573565fe54862860+19419515+E4cb466d568e663b386014080e11bdd9e22db32b6@5441920c 3632b61819853b163036e1f402638c44+1079105+E8db9e3652cc1780bebf800344e250feb52ff1f11@5441920c 22c064b8de458f72fb77e43f73cf3123+40594325+E672dbd4b2b496ef28b64ed6910948c68b607e491@5441920c 3c5710662d8d0e6f12b2071426d48644+5163249+Ec6517c5bd09f4b081dd95ebee4bd869f89b1441c@5441920c c3f52592b3f97416b23f689536e37693+17064012+Ecbcee67800458b6df98ff689187446966d821f39@5441920c 8456c6cfb316bf82c7934d3ced09b5b7+4703673+E916ece3d7117b6dce14e2e1621566cbd7766de3e@5441920c cbb9f866c562655729c4bf5f67666e46+20937649+E2d2396b6587f6f4ebf76295728070c835d55bfcb@5441920c 7eb39636607c3c8b726d67e928d0c950+19766577+E16676995d471f8d36806e8065063e9144e612d6c@5441920c 6873797eedef2fcd226686765b83cd84+16520799+E4de63c7082f1d2ccfc77457150423562d9346b62@5441920c 16c2450d3153b864de69e362c16ec6d4+20064956+E3b681fe29ee6c47e19ed1bc08947576d38c1b1c6@5441920c ce951e19024eb6ef606629527d03657f+14563555+Ebb1556509137131bec2c637946440d2e39f2dfeb@5441920c 968f09f24f240b0ee3de2615905d284c+17666235+Ec08966e756c198656d867620466617cd3d1021b7@5441920c 01264be9f7569fc6d446c6658c68c7ff+16200150+Efb257061168310d1334e51db7d064d13f053b7db@5441920c 3354d97c0c7368349cb167d463ffff3c+15699664+Ecdfe2e0b6c86dc12f8ed0c035056143dfdd16bc7@5441920c 685b0eb9860422e6c308197912f9cf89+26566964+Ee1c91b8b47f186dee67bc0bfc8581487f1734841@5441920c 10d6fde917d2f67974c342f2bdb99810+16666607+Ed9d736c3598219d786bccdc4480ccb6574fe65d4@5441920c 7436e9b3dc58d16c4f7fec9558e2e3f4+34299516+E84f366efbc7f687c37bd4006c80bf867606bbc2d@5441920c 0c7dc3f9be85bf7f02b6986369e15396+5167494+E3491f6f0157be9976e8f52f48f427068efef2041@5441920c 853860b5e3d7d68db870d64887eee036+9550459+E22296006714611dbf6ff2100006d14f7ee49274b@5441920c f4b3b1b8c22d36c1b2efdd626c3f7353+9425652+E502bd665962785f678f6e33e9b79ff5c09dbb892@5441920c 46dd6e718e7bc94962d460890d532d46+52257569+Ebe11c76489465f24695550366c829b11679d9f4c@5441920c b72f5e0756c4b5fd3ebdd71812f3ee56+6929925+E242c9f670807672f3b9cc681b47140529f436874@5441920c ed7f65db39984d581c595cd0e1e9d056+17556391+E8486351c5ec074e0b844c186d66fe701e44e3763@5441920c 63006056e077e0dc7716bf3425010ecc+14247713+E664b6f21cf6debc0095276164d3091f20b752597@5441920c d5116b69973d889d6f298f4738deb498+11066306+E3b4646b51c989f65567426664efde6e6c341f66c@5441920c 13e923c021e62ee066ef759d74e32d92+32067066+E3effb2cb94884161514b9cef413cc81b178be806@5441920c 65078f352fbbd13b8b56ebd0defb5cc1+10666222+E68f108063ebebded3649026b960f55f646c9b3f4@5441920c 3d4be63f60347bb56626be3969de967b+16626376+Ed8f748f6f073e373f71126f1c984815b26607dc3@5441920c f90b18dee1e00c275c2e238eb0393064+7956919+Ebd4f603319ed5e1f01c3e5875391688de2627899@5441920c b203671ec56f6d0bd72ff3f8415091e5+16713509+E5e929bb716e7dd51eef64530b23257d64dd06d64@5441920c 73b1ec2c6b3358b8190bf6c23e4569b0+16935900+E66185731649cf69f5d192b084b03dbef866ded63@5441920c 18697cdc7111d7dcf188dc222dc236d8+261032+E4b9dd594df44bdf4eb8850bb2f7dd1154b2fc5c8@5441920c 466b81f8768877dbc09fced3669fe11b+4269160+Ed51deef9b87c6c468b8cee2f1c7354f15117df62@5441920c 56719bc3f4db387ee926e85f9c017bb2+22617045+E182150854d000ed3316429530534337731b1c888@5441920c b6560f6d5e974464461f5d996cc16160+6953071+E26ddf53265f8146ed70f620d46f56667fb6e6411@5441920c 61280c9751d006c822044302870516f6+26475163+Ebe9b8b46c367107632bdf064fb80566ec8175e10@5441920c cd36ebee7f20ebb43dbe61351c9e33d8+21260557+E84b512df1b769c965f796560616566d36ed612d9@5441920c eeb7dd91c17fe167d870676648891ee1+47650592+Ed857b3e69858537d67766568e6e43d8b6487108c@5441920c 62ed43cd619e5307f96ec7294634ff81+9264520+Ef6ebc37b8877018c46c43f31b322f46b8676096f@5441920c 637d2d6c28466b2fbcc3596e4e48e925+15247646+E69510297f56571313d71633767be496d6ee5bce7@5441920c 1955419f9b01bd0e663edc61ef23fb44+6560616+E8eb1266643ff694fb8b2b2062469736302211e84@5441920c ed3b97847e816871458df4097897f666+26610427+E91d463cbc8639e2982d83e5ef6d6676130112c29@5441920c cc560418ec87624942f357bd6e349f11+27671122+E6326b03674bd7db8bd951d008d8e8617c64b959b@5441920c 346d8222e5e662b9d52d535e6354b571+3665630+E971cd85296175789601c66de54420bc0b04e58dc@5441920c 974beb90b949dc76258e7d73b6505e86+14940321+Ed73ffbc16e630c20b681536c7c6446c6fb6253ef@5441920c 6f88628c91767558e376f10d6eefb559+12957633+Eee6b1166f4c4fd01b9b39d84f85fc8bc68c7fb52@5441920c 50016d6fe13d9dc340cc27b6c20d6040+36096753+E6027b3b3d25bcb41de50469c6d8f6576613e6cf7@5441920c 66c0d207c66e6fbc509f52b8bf20b664+14674016+E206b24966155d3ce169076f32c91ef17c3bf7c85@5441920c 467e1966e64f17f6c68be65561c1dd6d+19901201+E8f59b255569d94803750e9c98e29c335218bde60@5441920c 3669c6c63e8ec00fde480790eff80647+14314479+E857690ec233c9c4b6436ef04590e21ceb26e7606@5441920c f924d760468f7dd3d28940f57f77193c+17691663+Ef8d9fbb6446528ff1c84bfbd61bfb4e2e9074fcf@5441920c 437442184168c16035eee82204cdf366+10632652+E715f84e7ff6f6d9d6b456d8854ec6b78403205f9@5441920c 4b2ebfccf47699f59192894db210d37e+5606647+Eb16336fe5036868c36db8cfbe0be660c4611676e@5441920c 9bd8ccfe078b86dc475656911667dc24+11677064+E1245c040830dc83b3e8ff5648296ff1e0bec36e7@5441920c 628384646666c65c0c67f9e671e67262+29615252+E3efcec0c36df19663d3c7240634740feb051d6cb@5441920c 04676d96e3b5dcf5e36f633c124b366c+24913006+E6461330125be80553808e043915b51c31567be17@5441920c 2e5cc96bf6d6c81674364bf74534d96d+35077014+E408515ceee93c3781b012517294560695b2d3ff8@5441920c 625d6dcc95bf40d68d42bc4f0d8c8135+23967236+Ef8f0869bdbfd658032d59d02692f6b4671d67b16@5441920c f7f176fd25d26d69057e4259f7164280+23454742+Eb05113506e03958136f1e77b659c805e481346b6@5441920c e0c006f2dfd4542e2b6b9d66c065975c+12476502+E6e617df6b90f7f1633b960cb710e5d639f507684@5441920c 65476f45e197d0cb38bcb0e5c3eb88c6+21624407+E75496e0662883f622ceb1166426172b11f066049@5441920c 46d6d4e5356fff3df952c006c60e605b+14556946+E2478fe7b5334c38c666519e26085f8f879f2e5ff@5441920c c06b49d30b4c58d6c407d0f01d8c9134+23503963+E6f100f01e53944593d14668b674e766eff14d626@5441920c 9ee892068c2c07664f13e519e2356f65+23959972+E5e5566d7092060b6e573d9b6058569967936f8f1@5441920c 8063f4732047fd6290456863c75355f8+13023330+Ef5e6fd0bd4d9e3e619769122400e6699ef42ffb6@5441920c 6133e2fd32713cd037fee9ee60193360+4304022+Eb9181ee3eb8d773d3929b1e58f605ef46158b668@5441920c 06cb6265f8ec862ee6c405b9c5185ff5+32133100+E2d828480e29d35725418721e6d969066fe6310c3@5441920c f9ed6b5c26c126de295bf1232ccedbdb+11110146+E8c90f9ddd3b6cd1620f48e34b7490534c43f7e74@5441920c 6bb1e5ccf8c4ef2583efe96bccf70491+17322291+E196662fc86919b65b4d92890cb6d758510fe0d6b@5441920c 32543c512ede456e6d526780b88b6b15+3523440+E4c9067c126d27c21558d66b676b9050c020feccd@5441920c 3884892318510d6e5e52e6194686d19d+6666009+Ebdb05b4e5c10f28d5507ce38c81c268cd3457084@5441920c 42bf30ee736e0e254b04e9e6913e06be+21279449+E1b145961d7c6637bfb1916eb42f503b4680636d8@5441920c 05f010fe0e3687f66986159dd1916699+2064644+Ed52318d3e26290f188266514dbd9602e779bb4d3@5441920c 662c56b8642e26265093b826e0689288+14665421+E36cdfe7b5656d9b7158e8ee7883886d1d34d52f8@5441920c 6789d9df635d40dbdb42cf201796619c+31742006+E496c7611143e3960f1c1c6c82fb16d8620dc7cd3@5441920c dcfe9fd63c4f21c8eedcdf27d60477bb+3139055+E91e6d07fccb75cc809bf6d5bf06bb2585ff0e8eb@5441920c f69eb3f93d80bf4f7221794964081e46+20025220+Ee596640d0963153b29b2edf074fd8c431c4b3f90@5441920c 967d6d4ed233c610896fc1d98c3b28b0+10164650+E81e99e39e3557b64076e433547541397b1478685@5441920c 9039b674c5217c1c2cd912f6e5028439+22530516+Ebb526c19b663ce48e1c6016634928684b7374b63@5441920c 14d6d4915152e2ce983496774fcde566+13062626+E22f59b087fbbe5eb5de5f7f66176b6bdcecb4940@5441920c e7060435500d777946081cd8df43c78b+17612660+Ec32fdd296301d5673c5415ef79c25e7fcdf1dfde@5441920c 9e31b4720c6ed8d60c4757065491f965+23191965+E67b454334b6140627c3fe06fdbe6028499970f63@5441920c d26d94682c04132c5c42709463ccc26e+9532662+Eb2cc826866265f6b9d72606fd6f5b250c61e9c7e@5441920c 4077d037e9c6096d6388e967236fc9e3+23463610+E93c7660c4f0defb357f973e15663e50b016ed35c@5441920c 939efcf9d66d076f1f52dbee0b46d886+11316441+E193158f0e66c8369d87e5561c720f355ee77c987@5441920c 78edd8c79e4911de2266411947056c8d+33406606+E632b24bd669f2c39ef56540fdc4fd90f5460303c@5441920c 2ff0eb23186b6db3c3f296363de5180c+29555310+E0139db36506e657b6995fd4771019d393f82885c@5441920c 5b0fe6d6604951cd5f6c78f66f359c66+13315726+E295b6db03207f03143e00edc9964739cffc195ce@5441920c 22d56132b728471e2712561b5e683548+24175332+E426de3f93b2bcbde645b57970b0663c1b7fb665f@5441920c f5092e3541b32c9b8553c18fc75deb59+30526779+E5f26e773f219367c550c67c6902b7e929d65e7d3@5441920c d43ec61d1ff5e63b9076626671b0c038+22764166+E915f309339169f6e23c4336240c6e592e08313e6@5441920c e6f4908ec7ec6060c8d466beb076d87d+6501650+Ed2502bebd746dc46382c87cf196c4bb468939782@5441920c 3b0d1c2743570c67ef61cc5940e60bff+30459907+Ef4258964867743bcb19f8ec64e66d018e865f452@5441920c fd47e84763e40011d00b23eb19cfe0d6+7937153+Edfc4e6bf57c0f1df165661393f86b1d355bd4d44@5441920c 34987c6857bce32f07bf4ee618772df9+17300095+Ed03738e8fdf1041662244f4270066862ff1fb197@5441920c b5356f66d692218e690bb94561c047fd+25176211+E0643cebe06c4ebe5fe1c5dbf746e2868e24b9c6f@5441920c cb9e15151f73d81f6864d55758496cc5+22053170+E6962e9ee716ce012e916f2cc93cc9624ce090225@5441920c 687f6d297b07065c613e6192963e82f0+26170716+Edd7828372c78ec4964d9363891068e13f339124f@5441920c b3e5368d95d1916d5653b90cbe4c5166+16915964+Ee39ebeb6e92096ee58648c66d373e5035b19fb96@5441920c 028f92d2328d0b82662e85c504389ebf+33011109+E56f2d7d585901e563e6223b63444806861778686@5441920c 8fee741b744b81dc001986562c76e2e3+34615593+E1e92ec6070bb4561382d33d7049dec660d71f87d@5441920c 0561c687debcbf5cd5bb661d576d88dd+22721666+E5fb1596528cbb8bc42d1d18cc8f0b46c5e0de6f5@5441920c b41703b42df6d3cd992599e9677b7c0c+21656716+E888d9325f666683427068d89bfc54806d296b684@5441920c e88dee4736fb1332fd0624d886984839+7279296+Ee91142f0e8f0fd9678e3e188effdc440e1d23408@5441920c 219dc65085f06379262dd3b4727b6efe+40036264+E9865f1497fd07c02915697e223727563686663ff@5441920c 8632264fbc7898588dc38e4639496636+9223066+E9c265e5654eccb4def668d87e0614465b4ec44bb@5441920c 10c33b498107392c30e1c5f3494e602d+7265467+E417be03290b37248c4c91fccd30b1b4692266255@5441920c c21959f5756d956456bf8f9eb955c4f0+11169673+E85e840f2b4870c83264233dc08fc3b396c9d1de2@5441920c 96db807ff3e36722740dddc5c1bf62e0+15396551+E1e8f4b9270dcf3638cb6182254c9df3d0e26221e@5441920c bbbf2e45768169c6699cccd60655b635+10659291+Ed47d66136074c5764fe655cb7d96b251ccbc4561@5441920c bfd61f58437b8b8021132efec2446665+6096913+E916f0429b8e8cc046dded7b8c07f4837ff021b5f@5441920c 37604e70419598750ec924d525267f3c+7660539+E6c4d798bf8eb3122157d31e0306c1e545611b259@5441920c de66fe2dcdb83b476990ecf1fbb27c26+11204600+Eb81c3d896180706f813d67882bd113fd69cf6528@5441920c 342eef125628740b9562673bfd2b4d96+54366+E70098b304b0d8975c36075254076223fb73f2eeb@5441920c 1b9215689eb58c256528bd2865c2d626+3466752+E127268472ff8bde9ee818669f4629298e8086bed@5441920c 086034465e0316e2648f8e4802604f51+31370255+E6f34120d24c1f3665846204865b4bcceb47686fe@5441920c 2e234e3f091bec21456c9cff0bc761b6+6507254+E6fb62e6b9e0e734d152106d060ee6692b6c9ff16@5441920c d6c63e474cf6b5385104b0b78677ec67+13175993+E9809c18b776b605849b2c321928863c362988576@5441920c 4ddfb1ccc7611289d7264bd70cc93dcf+12766634+E3b762696bee4dd0569d0302f957868fc20f51652@5441920c 0bdf4063360d6b021e7bd3ccdef516b7+9460636+Ec07113cdb6264b0821504ccf3b0e2604c1870916@5441920c 491e0110808fffddc6b9d2576c19dc1c+27323515+E11f2c06e4f100f75448161462cb693e6debd5178@5441920c 4f16e91e756de766b9bc8ce99900623e+9640416+E06208cbec42bfc0d55661e91036cdb4cb5dce80b@5441920c 5605f670b81c38565483275336c3eb92+15604264+E7ec502fe6fb951904769d67621100686144d56de@5441920c 176d89f315f6d49fee66317f081634c6+14067052+Ec6345f6d94364e6e310655435b476c46087e6746@5441920c 89b45833d2c19fd864538c2ec1d39db0+17922209+E6679c164b6f598d5d0630b678297cd068c9c9262@5441920c b42f550fc4f7c3987413b19e69ec784d+6951532+E589684c52326864cd096ce66dc61e30ebe4130d8@5441920c 70462101bfe861f600cff25705738683+10179374+Ee8735f8d2e55946d6dbd3622bfd0b52ed4ff5645@5441920c e5639f1ce89f30d7647d43d92f3749c9+13769767+Ecd88f922b0db4102564b81fc91c7b74f66112656@5441920c 922c4f0efe4d505f216cf6b16e0c74f0+13596264+E521d6ccf9306e12e3976c9169c122220b1cd702d@5441920c f75c903f8d88d6969e7ff2c72e5b31b8+22691146+E0f3dfdb223b828723e3017ff77e3f66b493b86dd@5441920c 6788dc29696632e5f39b668e84337147+16625559+Eff07ef424de0e25ff25316c43ecb9620c8ff6cd6@5441920c 195872d8776fb7df2699106f22de52eb+29592637+E3796e413486c57e1671b9066cf91fb6f358e1b8c@5441920c 1872d8876c16d6c72b1915486c996f51+16101070+E299d2660721262061d20e5421d387c966595f396@5441920c 6d54122d77b2246369d35f699220bd41+16610443+E763f085d9b08d8333e5d95f295028d5b848fc7b7@5441920c 9987e8842471d306ff54b68333fc94bc+14696064+E66641522d69ffb1b990be462106b248c99506b55@5441920c 37ed73f77c77c6c8ec8666e753cbbf7b+25736556+E87781259d92d670966e1654d369ee46d86d5ce66@5441920c de0dbcf70d224c16dcf92905ec10e261+17151669+E4228816e8d6d28e8835d8dfb46e54dc1f63c7c67@5441920c 709e8ee867526b180b619b682159c277+23262243+E93f00c26b28e85d26e8cec6d916de796e6e3333f@5441920c 374f74d9f4f0409b19ef96d00b267868+15933520+Eb4f5933760625f77d172235bb2fd62b5d46c1b6c@5441920c 51202e99c801cfc3062bd9610c00f063+2539339+E26c714978b06906d7144158b6ebb1fbe36d56344@5441920c b6fd759cb167c94557649cb3f7482d49+26353605+Ee642b25b5c00040520fe3dddd988c146e632cc14@5441920c 94669028355369bfe0db926846bb56f2+9695904+E94bd0c5fbe063be26b5d37061e0d5e13666b67d5@5441920c 2350567d203eb82066ef6dd59351990f+7647526+E27c8f695b3d508984bb35cdb78f75b0b690e5078@5441920c ed1f536d97255d9b3287612ed4833026+19420966+E35cd0f0303cfc68077376266e3117c72b369b10c@5441920c bf202c423c2f658db116976b3866c622+12634376+E7086c00ef933ccf0f07f0c9d00377797f337fefe@5441920c 17055b910c95c42619109362966c8fbf+10157396+Efd6d11c193bd32c7c69df08d8217ec63cf8414e3@5441920c f11c6144838cb9b4d67351d6626d1802+7156443+E4d5dcdd8ed1c174076dfd46767996651f38c4903@5441920c 9cb47df53d1cff53cd5b4796d0bc23f3+29952199+Eed564950d188541356161227068fd9f40fb5933d@5441920c 2ede654beb747fe9ee17be9dd5d3949c+12640911+Ee99ffeb440dd729067c606762ec076e524d592f5@5441920c 9be26457c84576c7e66e3168fd979607+26005247+Ec94b90868305fb875497f3b687655fd096e95296@5441920c 38b141749fdcc96dc28f725593486bec+61264+Ebe8cc5cfd0bdd54732ff1c62f6620ce4c797cbc8@5441920c 853b659766fbc96f641bc6923d5694bb+14544713+Eb567982c333b291b2d72467b6c431cee1bfcb6de@5441920c c6ecf79b145527c6cd62b8b6cf6f51b8+24455427+Ebc7bd846b936b266f7985111223eb1fb73d99cd5@5441920c 753b0b93996c2970915290ebb7eebd27+19979357+E3c0604c2ec64edbbc8360e568601e1c6ecbeebc1@5441920c 8fcfbb2b43c14680bdff1e514210632d+15760934+Ed0265cebdd6c614709e8cb4295f353e36083b32f@5441920c 11e006c41883660d19e68df266fe4636+22066346+E08206865313e13d29662e02927f424c7c8ebf265@5441920c d55b9e552b90d8f54c84f620ecb73e2b+15463950+E31701420310310e677b648926644c3234d52f472@5441920c c6b76f0b30d2e1c48c345608961c6603+2245652+E1248cd543d2160eef37fe460402ff946e75d8d64@5441920c 4806216f9c2638b63e678d0d660d2409+6206011+E1e94089dbb7c14d892d7f6521fc36764f5c6d761@5441920c 47f36ce735ff98996762cb1245e2d97b+7456077+Ec7ddc386d614f46ce7bbd4db8fc8e2e0261f923d@5441920c 7e8e73b8655f80b3583507fb666c77e1+15544112+Edfdd6ce9b54bf6fd3d2e8116783289dd77532b9f@5441920c 675b0fd88758c546376314872801576e+19435511+E30b4e596f663f0b826b5208370246bd321bbd856@5441920c d69769253bb145bc162c6158e9675316+6640055+Ef9c9c2ccbb6e964c05fb79b1250b606d57e59164@5441920c 0e357377b64fe29c3806fbf96c946645+9325419+E1b8609b20f5fef67fc46ffe5046b9f86e883d6e7@5441920c 9f57b97c259fed92f637d5232dde6104+9611496+E7e2b4cd0562494cbec77f3f67eb55414266d8d50@5441920c 09f60e940e2b603004d6337b32665beb+42415433+E93636b065e97d59bbdb24bc7dff5145f618f64d9@5441920c 6276b65424d63984f8015782060647b6+6046575+Ecc7e42155e92667eb8499956d012fc67b674301e@5441920c c9ce65d27ed164502366f9f5ec6e3fdf+22647045+Ebbff16b79dd826b687464f496f630db769e4f267@5441920c c16f091009b6f237366d5554137509c0+7507452+E3099761fe738fd5ee6368dcb8f1871d9bc018673@5441920c e06b96b906460dc628310477ec136ed7+24532176+E467927670673306f4186e4298f594c2584625137@5441920c f4ff1289c81b231be38907b88e82e975+20702445+Eb06cd9434e0292e6650453656986dbee2e5517b6@5441920c 8ed4167cbc6998f76847f4504cc21655+5393310+E3216b6f606602517fc6102e663746762e348b261@5441920c ed96eee78bcd599609bccb890d19d1c0+25036697+E2855c621547f6508f06862739b1d3c98d502f60f@5441920c b10905f5fbde35f7764492472ef1296c+17526792+E2387540056d68b4f5370bf7cb01d8439c83fc571@5441920c 762ef6d6e967ef7de65eb2095005664c+39123936+E366b9e4e438991d75f6cbc63d66d4671b62dc13b@5441920c 58686918bf8226496969555356830d50+21530262+E08415f6366061839595597edf078cc42764ec929@5441920c 987cc9c5c66e600676ccb76827266b69+39763257+Eb8e06991c83ec041e86f2e563656c869b6237cd7@5441920c c5c010572d6fd5f3683b3f7452e88b2d+6637631+Efb665b8364468f891bf42622099c643c558534f1@5441920c 076d7008f20864612f7f5132c66b84ce+16073436+Ec6cf748b16cc57f7168c989e661346495224f661@5441920c 81115023d44583e3dd80c630e9eb3b95+21766601+E4456d3c5e1cedc36461269e8c84fe32e8882f0b7@5441920c 26e15cef932e661c163d65c53f3d7596+11316659+Ed328777b54e6570d8fb1067f00847290be9642d7@5441920c 2e9c846ce77c8d62e58728d948f32301+6626151+E6742654b169c78c2636ee26bfbbbd246f86ec811@5441920c 86d19f8cc3be48b90501605017b36579+25421420+Edebc6387dd9f7fed0d4bcf6696220087381e5404@5441920c 27e6162bc2c14c183953fe682fdf1525+36360466+E7c6ece51c0fbd20f6647230bbdbdc66c66860beb@5441920c c03d55167fb6714d78880dc460574091+36766715+E140799f4146c60857050b56e4ffc66693b576ec2@5441920c 631c6b6f09985860c7fed6048e76b716+11066673+E5db6df91202e3100c4577f4bb665474382f8811c@5441920c d62dd2616f00f463681e15ec3647cd58+13126734+E609f8229cdf8c9e9642dfd6e3167ffd076dedbb8@5441920c 8749dd87c0d6b1377909c58fbc45dded+15236795+E461ee6611937f46654806754353bd32961666056@5441920c df7e5e5e1dd4d9dc09d8bf35b5fe3f24+22561443+E8fffe5863e071f5becb24e9c4de0569c1d864ec9@5441920c 4738611fe367691dd44e18f3c8857839+11364640+Ef171c946e87f52ec2877c74964d6c05115724fd6@5441920c f9ce82f59e5908d2d70e18df9679b469+31367794+E53f903684239bcc114f7bf8ff9bd6089f33058db@5441920c 0:15893477:chr10_band0_s0_e3000000.fj 15893477:8770829:chr10_band10_s29600000_e31300000.fj 24664306:15962689:chr10_band11_s31300000_e34400000.fj 40626995:18342794:chr10_band12_s34400000_e38000000.fj 58969789:5087834:chr10_band13_s38000000_e40200000.fj 64057623:4284756:chr10_band14_s40200000_e42300000.fj 68342379:18665404:chr10_band15_s42300000_e46100000.fj 87007783:13536792:chr10_band16_s46100000_e49900000.fj 100544575:13714429:chr10_band17_s49900000_e52900000.fj 114259004:44743112:chr10_band18_s52900000_e61200000.fj 159002116:17555223:chr10_band19_s61200000_e64500000.fj 176557339:4386647:chr10_band1_s3000000_e3800000.fj 180943986:32161952:chr10_band20_s64500000_e70600000.fj 213105938:22400285:chr10_band21_s70600000_e74900000.fj 235506223:14028139:chr10_band22_s74900000_e77700000.fj 249534362:22042495:chr10_band23_s77700000_e82000000.fj 271576857:31053589:chr10_band24_s82000000_e87900000.fj 302630446:7357223:chr10_band25_s87900000_e89500000.fj 309987669:17709824:chr10_band26_s89500000_e92900000.fj 327697493:6148418:chr10_band27_s92900000_e94100000.fj 333845911:14689912:chr10_band28_s94100000_e97000000.fj 348535823:11964810:chr10_band29_s97000000_e99300000.fj 360500633:14904735:chr10_band2_s3800000_e6600000.fj 375405368:13400037:chr10_band30_s99300000_e101900000.fj 388805405:5685774:chr10_band31_s101900000_e103000000.fj 394491179:9646905:chr10_band32_s103000000_e104900000.fj 404138084:4640161:chr10_band33_s104900000_e105800000.fj 408778245:32455363:chr10_band34_s105800000_e111900000.fj 441233608:15940309:chr10_band35_s111900000_e114900000.fj 457173917:22488871:chr10_band36_s114900000_e119100000.fj 479662788:13741614:chr10_band37_s119100000_e121700000.fj 493404402:7619587:chr10_band38_s121700000_e123100000.fj 501023989:23222084:chr10_band39_s123100000_e127500000.fj 524246073:29868907:chr10_band3_s6600000_e12200000.fj 554114980:16511841:chr10_band40_s127500000_e130600000.fj 570626821:26095352:chr10_band41_s130600000_e135534747.fj 596722173:26538428:chr10_band4_s12200000_e17300000.fj 623260601:5595242:chr10_band5_s17300000_e18600000.fj 628855843:524638:chr10_band6_s18600000_e18700000.fj 629380481:20166758:chr10_band7_s18700000_e22600000.fj 649547239:10195576:chr10_band8_s22600000_e24600000.fj 659742815:26057104:chr10_band9_s24600000_e29600000.fj 685799919:14129943:chr11_band0_s0_e2800000.fj 699929862:27262406:chr11_band10_s43500000_e48800000.fj 727192268:11366584:chr11_band11_s48800000_e51600000.fj 738558852:4284756:chr11_band12_s51600000_e53700000.fj 742843608:6746810:chr11_band13_s53700000_e55700000.fj 749590418:21620368:chr11_band14_s55700000_e59900000.fj 771210786:9186489:chr11_band15_s59900000_e61700000.fj 780397275:8326193:chr11_band16_s61700000_e63400000.fj 788723468:12757371:chr11_band17_s63400000_e65900000.fj 801480839:12157116:chr11_band18_s65900000_e68400000.fj 813637955:10261919:chr11_band19_s68400000_e70400000.fj 823899874:40669605:chr11_band1_s2800000_e10700000.fj 864569479:24190274:chr11_band20_s70400000_e75200000.fj 888759753:10020619:chr11_band21_s75200000_e77100000.fj 898780372:44638330:chr11_band22_s77100000_e85600000.fj 943418702:13920977:chr11_band23_s85600000_e88300000.fj 957339679:22389141:chr11_band24_s88300000_e92800000.fj 979728820:22616388:chr11_band25_s92800000_e97200000.fj 1002345208:26439412:chr11_band26_s97200000_e102100000.fj 1028784620:4173314:chr11_band27_s102100000_e102900000.fj 1032957934:39884156:chr11_band28_s102900000_e110400000.fj 1072842090:11123032:chr11_band29_s110400000_e112500000.fj 1083965122:10756630:chr11_band2_s10700000_e12700000.fj 1094721752:10580316:chr11_band30_s112500000_e114500000.fj 1105302068:35565428:chr11_band31_s114500000_e121200000.fj 1140867496:14197081:chr11_band32_s121200000_e123900000.fj 1155064577:20758432:chr11_band33_s123900000_e127800000.fj 1175823009:15792191:chr11_band34_s127800000_e130800000.fj 1191615200:22249239:chr11_band35_s130800000_e135006516.fj 1213864439:18449708:chr11_band3_s12700000_e16200000.fj 1232314147:29052525:chr11_band4_s16200000_e21700000.fj 1261366672:23968312:chr11_band5_s21700000_e26100000.fj 1285334984:5944481:chr11_band6_s26100000_e27200000.fj 1291279465:20155513:chr11_band7_s27200000_e31000000.fj 1311434978:28292374:chr11_band8_s31000000_e36400000.fj 1339727352:37778620:chr11_band9_s36400000_e43500000.fj 1377505972:16720695:chr12_band0_s0_e3300000.fj 1394226667:13059459:chr12_band10_s30700000_e33300000.fj 1407286126:7673046:chr12_band11_s33300000_e35800000.fj 1414959172:5825767:chr12_band12_s35800000_e38200000.fj 1420784939:42976743:chr12_band13_s38200000_e46400000.fj 1463761682:13809906:chr12_band14_s46400000_e49100000.fj 1477571588:11988262:chr12_band15_s49100000_e51500000.fj 1489559850:17595626:chr12_band16_s51500000_e54900000.fj 1507155476:8587338:chr12_band17_s54900000_e56600000.fj 1515742814:7408989:chr12_band18_s56600000_e58100000.fj 1523151803:26345033:chr12_band19_s58100000_e63100000.fj 1549496836:11140028:chr12_band1_s3300000_e5400000.fj 1560636864:9977002:chr12_band20_s63100000_e65100000.fj 1570613866:13651023:chr12_band21_s65100000_e67700000.fj 1584264889:19846309:chr12_band22_s67700000_e71500000.fj 1604111198:22406679:chr12_band23_s71500000_e75700000.fj 1626517877:24370117:chr12_band24_s75700000_e80300000.fj 1650887994:34354522:chr12_band25_s80300000_e86700000.fj 1685242516:12153797:chr12_band26_s86700000_e89000000.fj 1697396313:19120741:chr12_band27_s89000000_e92600000.fj 1716517054:18678462:chr12_band28_s92600000_e96200000.fj 1735195516:28125462:chr12_band29_s96200000_e101600000.fj 1763320978:23263164:chr12_band2_s5400000_e10100000.fj 1786584142:11438933:chr12_band30_s101600000_e103800000.fj 1798023075:27434807:chr12_band31_s103800000_e109000000.fj 1825457882:13431932:chr12_band32_s109000000_e111700000.fj 1838889814:2833555:chr12_band33_s111700000_e112300000.fj 1841723369:10166739:chr12_band34_s112300000_e114300000.fj 1851890108:13335983:chr12_band35_s114300000_e116800000.fj 1865226091:6763178:chr12_band36_s116800000_e118100000.fj 1871989269:13444650:chr12_band37_s118100000_e120700000.fj 1885433919:26286416:chr12_band38_s120700000_e125900000.fj 1911720335:18376984:chr12_band39_s125900000_e129300000.fj 1930097319:14118184:chr12_band3_s10100000_e12800000.fj 1944215503:23892725:chr12_band40_s129300000_e133851895.fj 1968108228:10507783:chr12_band4_s12800000_e14800000.fj 1978616011:27625276:chr12_band5_s14800000_e20000000.fj 2006241287:7026139:chr12_band6_s20000000_e21300000.fj 2013267426:27711533:chr12_band7_s21300000_e26500000.fj 2040978959:6793207:chr12_band8_s26500000_e27800000.fj 2047772166:15405916:chr12_band9_s27800000_e30700000.fj 2063178082:9180724:chr13_band0_s0_e4500000.fj 2072358806:9467601:chr13_band10_s32200000_e34000000.fj 2081826407:7989532:chr13_band11_s34000000_e35500000.fj 2089815939:24739014:chr13_band12_s35500000_e40100000.fj 2114554953:26941582:chr13_band13_s40100000_e45200000.fj 2141496535:3036311:chr13_band14_s45200000_e45800000.fj 2144532846:7761096:chr13_band15_s45800000_e47300000.fj 2152293942:18709476:chr13_band16_s47300000_e50900000.fj 2171003418:22602285:chr13_band17_s50900000_e55300000.fj 2193605703:23405896:chr13_band18_s55300000_e59600000.fj 2217011599:14457382:chr13_band19_s59600000_e62300000.fj 2231468981:11220750:chr13_band1_s4500000_e10000000.fj 2242689731:18581486:chr13_band20_s62300000_e65700000.fj 2261271217:15834314:chr13_band21_s65700000_e68600000.fj 2277105531:26147285:chr13_band22_s68600000_e73300000.fj 2303252816:11193151:chr13_band23_s73300000_e75400000.fj 2314445967:9599462:chr13_band24_s75400000_e77200000.fj 2324045429:9625154:chr13_band25_s77200000_e79000000.fj 2333670583:46677445:chr13_band26_s79000000_e87700000.fj 2380348028:12795853:chr13_band27_s87700000_e90000000.fj 2393143881:27123199:chr13_band28_s90000000_e95000000.fj 2420267080:16832721:chr13_band29_s95000000_e98200000.fj 2437099801:12852756:chr13_band2_s10000000_e16300000.fj 2449952557:5708668:chr13_band30_s98200000_e99300000.fj 2455661225:12588075:chr13_band31_s99300000_e101700000.fj 2468249300:16946677:chr13_band32_s101700000_e104800000.fj 2485195977:12209370:chr13_band33_s104800000_e107000000.fj 2497405347:17916606:chr13_band34_s107000000_e110300000.fj 2515321953:24643337:chr13_band35_s110300000_e115169878.fj 2539965290:3264756:chr13_band3_s16300000_e17900000.fj 2543230046:4102134:chr13_band4_s17900000_e19500000.fj 2547332180:19703325:chr13_band5_s19500000_e23300000.fj 2567035505:11554223:chr13_band6_s23300000_e25500000.fj 2578589728:12130664:chr13_band7_s25500000_e27800000.fj 2590720392:5842000:chr13_band8_s27800000_e28900000.fj 2596562392:17354821:chr13_band9_s28900000_e32200000.fj 2613917213:7548724:chr14_band0_s0_e3700000.fj 2621465937:30306549:chr14_band10_s37800000_e43500000.fj 2651772486:19488657:chr14_band11_s43500000_e47200000.fj 2671261143:19588732:chr14_band12_s47200000_e50900000.fj 2690849875:16728188:chr14_band13_s50900000_e54100000.fj 2707578063:7297044:chr14_band14_s54100000_e55500000.fj 2714875107:13453405:chr14_band15_s55500000_e58100000.fj 2728328512:20891242:chr14_band16_s58100000_e62100000.fj 2749219754:13969727:chr14_band17_s62100000_e64800000.fj 2763189481:15929958:chr14_band18_s64800000_e67900000.fj 2779119439:12006715:chr14_band19_s67900000_e70200000.fj 2791126154:8976748:chr14_band1_s3700000_e8100000.fj 2800102902:18617309:chr14_band20_s70200000_e73800000.fj 2818720211:28602130:chr14_band21_s73800000_e79300000.fj 2847322341:22781826:chr14_band22_s79300000_e83600000.fj 2870104167:7096857:chr14_band23_s83600000_e84900000.fj 2877201024:26087198:chr14_band24_s84900000_e89800000.fj 2903288222:10873992:chr14_band25_s89800000_e91900000.fj 2914162214:14647560:chr14_band26_s91900000_e94700000.fj 2928809774:8587442:chr14_band27_s94700000_e96300000.fj 2937397216:27389311:chr14_band28_s96300000_e101400000.fj 2964786527:9264693:chr14_band29_s101400000_e103200000.fj 2974051220:16320752:chr14_band2_s8100000_e16100000.fj 2990371972:4140293:chr14_band30_s103200000_e104000000.fj 2994512265:17268099:chr14_band31_s104000000_e107349540.fj 3011780364:3060756:chr14_band3_s16100000_e17600000.fj 3014841120:3260428:chr14_band4_s17600000_e19100000.fj 3018101548:26138225:chr14_band5_s19100000_e24600000.fj 3044239773:45862056:chr14_band6_s24600000_e33300000.fj 3090101829:10447980:chr14_band7_s33300000_e35300000.fj 3100549809:6564588:chr14_band8_s35300000_e36600000.fj 3107114397:6398876:chr14_band9_s36600000_e37800000.fj 3113513273:7956724:chr15_band0_s0_e3900000.fj 3121469997:34269266:chr15_band10_s33600000_e40100000.fj 3155739263:13762411:chr15_band11_s40100000_e42800000.fj 3169501674:3947813:chr15_band12_s42800000_e43600000.fj 3173449487:5537714:chr15_band13_s43600000_e44800000.fj 3178987201:24305832:chr15_band14_s44800000_e49500000.fj 3203293033:17507515:chr15_band15_s49500000_e52900000.fj 3220800548:32826524:chr15_band16_s52900000_e59100000.fj 3253627072:1010299:chr15_band17_s59100000_e59300000.fj 3254637371:23454838:chr15_band18_s59300000_e63700000.fj 3278092209:18017355:chr15_band19_s63700000_e67200000.fj 3296109564:9792748:chr15_band1_s3900000_e8700000.fj 3305902312:533847:chr15_band20_s67200000_e67300000.fj 3306436159:1084858:chr15_band21_s67300000_e67500000.fj 3307521017:27465637:chr15_band22_s67500000_e72700000.fj 3334986654:12707353:chr15_band23_s72700000_e75200000.fj 3347694007:6832970:chr15_band24_s75200000_e76600000.fj 3354526977:8748794:chr15_band25_s76600000_e78300000.fj 3363275771:17732191:chr15_band26_s78300000_e81700000.fj 3381007962:15491375:chr15_band27_s81700000_e85200000.fj 3396499337:20295749:chr15_band28_s85200000_e89100000.fj 3416795086:27117670:chr15_band29_s89100000_e94300000.fj 3443912756:14484752:chr15_band2_s8700000_e15800000.fj 3458397508:22592925:chr15_band30_s94300000_e98500000.fj 3480990433:21043993:chr15_band31_s98500000_e102531392.fj 3502034426:6528756:chr15_band3_s15800000_e19000000.fj 3508563182:4646274:chr15_band4_s19000000_e20700000.fj 3513209456:19571328:chr15_band5_s20700000_e25700000.fj 3532780784:12923689:chr15_band6_s25700000_e28100000.fj 3545704473:9921926:chr15_band7_s28100000_e30300000.fj 3555626399:2895507:chr15_band8_s30300000_e31200000.fj 3558521906:11292446:chr15_band9_s31200000_e33600000.fj 3569814352:40629656:chr16_band0_s0_e7900000.fj 3610444008:4080756:chr16_band10_s36600000_e38600000.fj 3614524764:18810667:chr16_band11_s38600000_e47000000.fj 3633335431:29170320:chr16_band12_s47000000_e52600000.fj 3662505751:21574362:chr16_band13_s52600000_e56700000.fj 3684080113:3619563:chr16_band14_s56700000_e57400000.fj 3687699676:49161531:chr16_band15_s57400000_e66700000.fj 3736861207:19748144:chr16_band16_s66700000_e70800000.fj 3756609351:10946735:chr16_band17_s70800000_e72900000.fj 3767556086:6378485:chr16_band18_s72900000_e74100000.fj 3773934571:26881587:chr16_band19_s74100000_e79200000.fj 3800816158:13661669:chr16_band1_s7900000_e10500000.fj 3814477827:13501427:chr16_band20_s79200000_e81700000.fj 3827979254:13677551:chr16_band21_s81700000_e84200000.fj 3841656805:15666076:chr16_band22_s84200000_e87100000.fj 3857322881:7998490:chr16_band23_s87100000_e88700000.fj 3865321371:8053236:chr16_band24_s88700000_e90354753.fj 3873374607:10728254:chr16_band2_s10500000_e12600000.fj 3884102861:11356748:chr16_band3_s12600000_e14800000.fj 3895459609:7600427:chr16_band4_s14800000_e16800000.fj 3903060036:20722736:chr16_band5_s16800000_e21200000.fj 3923782772:13729019:chr16_band6_s21200000_e24200000.fj 3937511791:20246913:chr16_band7_s24200000_e28100000.fj 3957758704:26945678:chr16_band8_s28100000_e34600000.fj 3984704382:3384870:chr16_band9_s34600000_e36600000.fj 3988089252:16155754:chr17_band0_s0_e3300000.fj 4004245006:12762477:chr17_band10_s38400000_e40900000.fj 4017007483:18572384:chr17_band11_s40900000_e44900000.fj 4035579867:12458663:chr17_band12_s44900000_e47400000.fj 4048038530:14524689:chr17_band13_s47400000_e50200000.fj 4062563219:38661662:chr17_band14_s50200000_e57600000.fj 4101224881:3149045:chr17_band15_s57600000_e58300000.fj 4104373926:13700211:chr17_band16_s58300000_e61100000.fj 4118074137:7529724:chr17_band17_s61100000_e62600000.fj 4125603861:7950542:chr17_band18_s62600000_e64200000.fj 4133554403:14756800:chr17_band19_s64200000_e67100000.fj 4148311203:16443598:chr17_band1_s3300000_e6500000.fj 4164754801:20108889:chr17_band20_s67100000_e70900000.fj 4184863690:20058363:chr17_band21_s70900000_e74800000.fj 4204922053:2587408:chr17_band22_s74800000_e75300000.fj 4207509461:30547504:chr17_band23_s75300000_e81195210.fj 4238056965:21562054:chr17_band2_s6500000_e10700000.fj 4259619019:27395356:chr17_band3_s10700000_e16000000.fj 4287014375:28365678:chr17_band4_s16000000_e22200000.fj 4315380053:289200:chr17_band5_s22200000_e24000000.fj 4315669253:5237174:chr17_band6_s24000000_e25800000.fj 4320906427:29727146:chr17_band7_s25800000_e31800000.fj 4350633573:30907874:chr17_band8_s31800000_e38100000.fj 4381541447:1504858:chr17_band9_s38100000_e38400000.fj 4383046305:14943044:chr18_band0_s0_e2900000.fj 4397989349:33721037:chr18_band10_s37200000_e43500000.fj 4431710386:24805551:chr18_band11_s43500000_e48200000.fj 4456515937:29378907:chr18_band12_s48200000_e53800000.fj 4485894844:12633635:chr18_band13_s53800000_e56200000.fj 4498528479:14797428:chr18_band14_s56200000_e59000000.fj 4513325907:13780102:chr18_band15_s59000000_e61600000.fj 4527106009:28794272:chr18_band16_s61600000_e66800000.fj 4555900281:10201924:chr18_band17_s66800000_e68700000.fj 4566102205:24124836:chr18_band18_s68700000_e73100000.fj 4590227041:26615557:chr18_band19_s73100000_e78077248.fj 4616842598:22145236:chr18_band1_s2900000_e7100000.fj 4638987834:7311348:chr18_band2_s7100000_e8500000.fj 4646299182:12577740:chr18_band3_s8500000_e10900000.fj 4658876922:21508140:chr18_band4_s10900000_e15400000.fj 4680385062:52389:chr18_band5_s15400000_e17200000.fj 4680437451:5076969:chr18_band6_s17200000_e19000000.fj 4685514420:31190178:chr18_band7_s19000000_e25000000.fj 4716704598:41160388:chr18_band8_s25000000_e32700000.fj 4757864986:23815045:chr18_band9_s32700000_e37200000.fj 4781680031:34031899:chr19_band0_s0_e6900000.fj 4815711930:13851503:chr19_band10_s35500000_e38300000.fj 4829563433:1998048:chr19_band11_s38300000_e38700000.fj 4831561481:22892591:chr19_band12_s38700000_e43400000.fj 4854454072:8872354:chr19_band13_s43400000_e45200000.fj 4863326426:13749381:chr19_band14_s45200000_e48000000.fj 4877075807:16660930:chr19_band15_s48000000_e51400000.fj 4893736737:11038031:chr19_band16_s51400000_e53600000.fj 4904774768:13412850:chr19_band17_s53600000_e56300000.fj 4918187618:14313555:chr19_band18_s56300000_e59128983.fj 4932501173:33635703:chr19_band1_s6900000_e13900000.fj 4966136876:489834:chr19_band2_s13900000_e14000000.fj 4966626710:11377056:chr19_band3_s14000000_e16300000.fj 4978003766:18348545:chr19_band4_s16300000_e20000000.fj 4996352311:21127772:chr19_band5_s20000000_e24400000.fj 5017480083:1059388:chr19_band6_s24400000_e26500000.fj 5018539471:6984270:chr19_band7_s26500000_e28600000.fj 5025523741:20073973:chr19_band8_s28600000_e32400000.fj 5045597714:15769669:chr19_band9_s32400000_e35500000.fj 5061367383:9756229:chr1_band0_s0_e2300000.fj 5071123612:11489333:chr1_band10_s30200000_e32400000.fj 5082612945:11074951:chr1_band11_s32400000_e34600000.fj 5093687896:28145091:chr1_band12_s34600000_e40100000.fj 5121832987:20545569:chr1_band13_s40100000_e44100000.fj 5142378556:13582476:chr1_band14_s44100000_e46800000.fj 5155961032:19737049:chr1_band15_s46800000_e50700000.fj 5175698081:27529030:chr1_band16_s50700000_e56100000.fj 5203227111:15452164:chr1_band17_s56100000_e59000000.fj 5218679275:12082565:chr1_band18_s59000000_e61300000.fj 5230761840:39789591:chr1_band19_s61300000_e68900000.fj 5270551431:15804689:chr1_band1_s2300000_e5400000.fj 5286356120:4141822:chr1_band20_s68900000_e69700000.fj 5290497942:80211445:chr1_band21_s69700000_e84900000.fj 5370709387:18343642:chr1_band22_s84900000_e88400000.fj 5389053029:18664730:chr1_band23_s88400000_e92000000.fj 5407717759:13861818:chr1_band24_s92000000_e94700000.fj 5421579577:26472421:chr1_band25_s94700000_e99700000.fj 5448051998:13161786:chr1_band26_s99700000_e102200000.fj 5461213784:26136584:chr1_band27_s102200000_e107200000.fj 5487350368:23561374:chr1_band28_s107200000_e111800000.fj 5510911742:22349851:chr1_band29_s111800000_e116100000.fj 5533261593:9400437:chr1_band2_s5400000_e7200000.fj 5542662030:8898401:chr1_band30_s116100000_e117800000.fj 5551560431:14463385:chr1_band31_s117800000_e120600000.fj 5566023816:2797932:chr1_band32_s120600000_e121500000.fj 5568821748:7140760:chr1_band33_s121500000_e125000000.fj 5575962508:7956760:chr1_band34_s125000000_e128900000.fj 5583919268:28100130:chr1_band35_s128900000_e142600000.fj 5612019398:15570132:chr1_band36_s142600000_e147000000.fj 5627589530:12079936:chr1_band37_s147000000_e150300000.fj 5639669466:23848498:chr1_band38_s150300000_e155000000.fj 5663517964:7320072:chr1_band39_s155000000_e156500000.fj 5670838036:10249929:chr1_band3_s7200000_e9200000.fj 5681087965:13622024:chr1_band40_s156500000_e159100000.fj 5694709989:7329847:chr1_band41_s159100000_e160500000.fj 5702039836:25915639:chr1_band42_s160500000_e165500000.fj 5727955475:8902437:chr1_band43_s165500000_e167200000.fj 5736857912:19387309:chr1_band44_s167200000_e170900000.fj 5756245221:10334901:chr1_band45_s170900000_e172900000.fj 5766580122:15956391:chr1_band46_s172900000_e176000000.fj 5782536513:22381464:chr1_band47_s176000000_e180300000.fj 5804917977:28762910:chr1_band48_s180300000_e185800000.fj 5833680887:27482517:chr1_band49_s185800000_e190800000.fj 5861163404:17698144:chr1_band4_s9200000_e12700000.fj 5878861548:16115379:chr1_band50_s190800000_e193800000.fj 5894976927:26603399:chr1_band51_s193800000_e198700000.fj 5921580326:42767332:chr1_band52_s198700000_e207200000.fj 5964347658:22519054:chr1_band53_s207200000_e211500000.fj 5986866712:15623994:chr1_band54_s211500000_e214500000.fj 6002490706:50651137:chr1_band55_s214500000_e224100000.fj 6053141843:2340783:chr1_band56_s224100000_e224600000.fj 6055482626:12296366:chr1_band57_s224600000_e227000000.fj 6067778992:19160541:chr1_band58_s227000000_e230700000.fj 6086939533:21150112:chr1_band59_s230700000_e234700000.fj 6108089645:15934102:chr1_band5_s12700000_e16200000.fj 6124023747:9572247:chr1_band60_s234700000_e236600000.fj 6133595994:37063925:chr1_band61_s236600000_e243700000.fj 6170659919:28279658:chr1_band62_s243700000_e249250621.fj 6198939577:21312883:chr1_band6_s16200000_e20400000.fj 6220252460:17968553:chr1_band7_s20400000_e23900000.fj 6238221013:20502272:chr1_band8_s23900000_e28000000.fj 6258723285:10454348:chr1_band9_s28000000_e30200000.fj 6269177633:26240932:chr20_band0_s0_e5100000.fj 6295418565:11477343:chr20_band10_s32100000_e34400000.fj 6306895908:16121702:chr20_band11_s34400000_e37600000.fj 6323017610:21665969:chr20_band12_s37600000_e41700000.fj 6344683579:2106601:chr20_band13_s41700000_e42100000.fj 6346790180:22234896:chr20_band14_s42100000_e46400000.fj 6369025076:17466445:chr20_band15_s46400000_e49800000.fj 6386491521:27353500:chr20_band16_s49800000_e55000000.fj 6413845021:7951115:chr20_band17_s55000000_e56500000.fj 6421796136:10132647:chr20_band18_s56500000_e58400000.fj 6431928783:24122390:chr20_band19_s58400000_e63025520.fj 6456051173:21750808:chr20_band1_s5100000_e9200000.fj 6477801981:15548705:chr20_band2_s9200000_e12100000.fj 6493350686:30792695:chr20_band3_s12100000_e17900000.fj 6524143381:17804912:chr20_band4_s17900000_e21300000.fj 6541948293:5184960:chr20_band5_s21300000_e22300000.fj 6547133253:17298739:chr20_band6_s22300000_e25600000.fj 6564431992:3301773:chr20_band7_s25600000_e27500000.fj 6567733765:3876756:chr20_band8_s27500000_e29400000.fj 6571610521:13283209:chr20_band9_s29400000_e32100000.fj 6584893730:5712724:chr21_band0_s0_e2800000.fj 6590606454:10518888:chr21_band10_s35800000_e37800000.fj 6601125342:10144603:chr21_band11_s37800000_e39700000.fj 6611269945:15620599:chr21_band12_s39700000_e42600000.fj 6626890544:28940326:chr21_band13_s42600000_e48129895.fj 6655830870:8160748:chr21_band1_s2800000_e6800000.fj 6663991618:11144287:chr21_band2_s6800000_e10900000.fj 6675135905:1431977:chr21_band3_s10900000_e13200000.fj 6676567882:2244756:chr21_band4_s13200000_e14300000.fj 6678812638:9266581:chr21_band5_s14300000_e16400000.fj 6688079219:41245659:chr21_band6_s16400000_e24000000.fj 6729324878:15344510:chr21_band7_s24000000_e26800000.fj 6744669388:24932791:chr21_band8_s26800000_e31500000.fj 6769602179:22442446:chr21_band9_s31500000_e35800000.fj 6792044625:7752724:chr22_band0_s0_e3800000.fj 6799797349:28224380:chr22_band10_s32200000_e37600000.fj 6828021729:17304839:chr22_band11_s37600000_e41000000.fj 6845326568:16113075:chr22_band12_s41000000_e44200000.fj 6861439643:22233411:chr22_band13_s44200000_e48400000.fj 6883673054:5524922:chr22_band14_s48400000_e49400000.fj 6889197976:9664262:chr22_band15_s49400000_e51304566.fj 6898862238:9180748:chr22_band1_s3800000_e8300000.fj 6908042986:7956752:chr22_band2_s8300000_e12200000.fj 6915999738:5100756:chr22_band3_s12200000_e14700000.fj 6921100494:9937902:chr22_band4_s14700000_e17900000.fj 6931038396:19548232:chr22_band5_s17900000_e22200000.fj 6950586628:6683394:chr22_band6_s22200000_e23500000.fj 6957270022:11752445:chr22_band7_s23500000_e25900000.fj 6969022467:19256022:chr22_band8_s25900000_e29600000.fj 6988278489:12954853:chr22_band9_s29600000_e32200000.fj 7001233342:23233415:chr2_band0_s0_e4400000.fj 7024466757:10667298:chr2_band10_s36600000_e38600000.fj 7035134055:16966684:chr2_band11_s38600000_e41800000.fj 7052100739:31586877:chr2_band12_s41800000_e47800000.fj 7083687616:26968370:chr2_band13_s47800000_e52900000.fj 7110655986:10993850:chr2_band14_s52900000_e55000000.fj 7121649836:33045521:chr2_band15_s55000000_e61300000.fj 7154695357:14150927:chr2_band16_s61300000_e64100000.fj 7168846284:23578835:chr2_band17_s64100000_e68600000.fj 7192425119:14885552:chr2_band18_s68600000_e71500000.fj 7207310671:10410131:chr2_band19_s71500000_e73500000.fj 7217720802:14156834:chr2_band1_s4400000_e7100000.fj 7231877636:7578172:chr2_band20_s73500000_e75000000.fj 7239455808:44109485:chr2_band21_s75000000_e83300000.fj 7283565293:31254935:chr2_band22_s83300000_e90500000.fj 7314820228:5169067:chr2_band23_s90500000_e93300000.fj 7319989295:10368921:chr2_band24_s93300000_e96800000.fj 7330358216:29052271:chr2_band25_s96800000_e102700000.fj 7359410487:17612827:chr2_band26_s102700000_e106000000.fj 7377023314:7641759:chr2_band27_s106000000_e107500000.fj 7384665073:13411716:chr2_band28_s107500000_e110200000.fj 7398076789:17757245:chr2_band29_s110200000_e114400000.fj 7415834034:26954567:chr2_band2_s7100000_e12200000.fj 7442788601:23246223:chr2_band30_s114400000_e118800000.fj 7466034824:19074161:chr2_band31_s118800000_e122400000.fj 7485108985:39449695:chr2_band32_s122400000_e129900000.fj 7524558680:11696577:chr2_band33_s129900000_e132500000.fj 7536255257:13249863:chr2_band34_s132500000_e135100000.fj 7549505120:8708592:chr2_band35_s135100000_e136800000.fj 7558213712:29182964:chr2_band36_s136800000_e142200000.fj 7587396676:10264945:chr2_band37_s142200000_e144100000.fj 7597661621:24601843:chr2_band38_s144100000_e148700000.fj 7622263464:5951781:chr2_band39_s148700000_e149900000.fj 7628215245:23795508:chr2_band3_s12200000_e16700000.fj 7652010753:3150007:chr2_band40_s149900000_e150500000.fj 7655160760:23077469:chr2_band41_s150500000_e154900000.fj 7678238229:25968072:chr2_band42_s154900000_e159800000.fj 7704206301:20640325:chr2_band43_s159800000_e163700000.fj 7724846626:31998832:chr2_band44_s163700000_e169700000.fj 7756845458:43632512:chr2_band45_s169700000_e178000000.fj 7800477970:13731959:chr2_band46_s178000000_e180600000.fj 7814209929:12856172:chr2_band47_s180600000_e183000000.fj 7827066101:34247127:chr2_band48_s183000000_e189400000.fj 7861313228:13286018:chr2_band49_s189400000_e191900000.fj 7874599246:13181256:chr2_band4_s16700000_e19200000.fj 7887780502:29663052:chr2_band50_s191900000_e197400000.fj 7917443554:30634366:chr2_band51_s197400000_e203300000.fj 7948077920:8075493:chr2_band52_s203300000_e204900000.fj 7956153413:21661204:chr2_band53_s204900000_e209000000.fj 7977814617:33806107:chr2_band54_s209000000_e215300000.fj 8011620724:32791910:chr2_band55_s215300000_e221500000.fj 8044412634:19689112:chr2_band56_s221500000_e225200000.fj 8064101746:4741805:chr2_band57_s225200000_e226100000.fj 8068843551:25904705:chr2_band58_s226100000_e231000000.fj 8094748256:23619321:chr2_band59_s231000000_e235600000.fj 8118367577:25423194:chr2_band5_s19200000_e24000000.fj 8143790771:9119290:chr2_band60_s235600000_e237300000.fj 8152910061:30796914:chr2_band61_s237300000_e243199373.fj 8183706975:19924674:chr2_band6_s24000000_e27900000.fj 8203631649:11135309:chr2_band7_s27900000_e30000000.fj 8214766958:10940177:chr2_band8_s30000000_e32100000.fj 8225707135:23560118:chr2_band9_s32100000_e36600000.fj 8249267253:14861122:chr3_band0_s0_e2800000.fj 8264128375:22809815:chr3_band10_s32100000_e36500000.fj 8286938190:15046818:chr3_band11_s36500000_e39400000.fj 8301985008:22186262:chr3_band12_s39400000_e43700000.fj 8324171270:2058080:chr3_band13_s43700000_e44100000.fj 8326229350:521252:chr3_band14_s44100000_e44200000.fj 8326750602:32234144:chr3_band15_s44200000_e50600000.fj 8358984746:8441932:chr3_band16_s50600000_e52300000.fj 8367426678:10948899:chr3_band17_s52300000_e54400000.fj 8378375577:21772898:chr3_band18_s54400000_e58600000.fj 8400148475:27069700:chr3_band19_s58600000_e63700000.fj 8427218175:6545313:chr3_band1_s2800000_e4000000.fj 8433763488:31787795:chr3_band20_s63700000_e69800000.fj 8465551283:23275812:chr3_band21_s69800000_e74200000.fj 8488827095:29739564:chr3_band22_s74200000_e79800000.fj 8518566659:20035093:chr3_band23_s79800000_e83500000.fj 8538601752:20162108:chr3_band24_s83500000_e87200000.fj 8558763860:3767584:chr3_band25_s87200000_e87900000.fj 8562531444:13581503:chr3_band26_s87900000_e91000000.fj 8576112947:7002557:chr3_band27_s91000000_e93900000.fj 8583115504:23576185:chr3_band28_s93900000_e98300000.fj 8606691689:8815871:chr3_band29_s98300000_e100000000.fj 8615507560:24882143:chr3_band2_s4000000_e8700000.fj 8640389703:4697534:chr3_band30_s100000000_e100900000.fj 8645087237:9838940:chr3_band31_s100900000_e102800000.fj 8654926177:18496118:chr3_band32_s102800000_e106200000.fj 8673422295:9018631:chr3_band33_s106200000_e107900000.fj 8682440926:17929166:chr3_band34_s107900000_e111300000.fj 8700370092:11594711:chr3_band35_s111300000_e113500000.fj 8711964803:20308668:chr3_band36_s113500000_e117300000.fj 8732273471:9030401:chr3_band37_s117300000_e119000000.fj 8741303872:14898827:chr3_band38_s119000000_e121900000.fj 8756202699:10008811:chr3_band39_s121900000_e123800000.fj 8766211510:15979710:chr3_band3_s8700000_e11800000.fj 8782191220:10116188:chr3_band40_s123800000_e125800000.fj 8792307408:17806797:chr3_band41_s125800000_e129200000.fj 8810114205:23227207:chr3_band42_s129200000_e133700000.fj 8833341412:10556009:chr3_band43_s133700000_e135700000.fj 8843897421:15182933:chr3_band44_s135700000_e138700000.fj 8859080354:21307590:chr3_band45_s138700000_e142800000.fj 8880387944:32759712:chr3_band46_s142800000_e148900000.fj 8913147656:16878434:chr3_band47_s148900000_e152100000.fj 8930026090:15100163:chr3_band48_s152100000_e155000000.fj 8945126253:10434017:chr3_band49_s155000000_e157000000.fj 8955560270:7785476:chr3_band4_s11800000_e13300000.fj 8963345746:10542610:chr3_band50_s157000000_e159000000.fj 8973888356:8787004:chr3_band51_s159000000_e160700000.fj 8982675360:37253134:chr3_band52_s160700000_e167600000.fj 9019928494:17183652:chr3_band53_s167600000_e170900000.fj 9037112146:25746921:chr3_band54_s170900000_e175700000.fj 9062859067:17296262:chr3_band55_s175700000_e179000000.fj 9080155329:19044817:chr3_band56_s179000000_e182700000.fj 9099200146:9216326:chr3_band57_s182700000_e184500000.fj 9108416472:7709847:chr3_band58_s184500000_e186000000.fj 9116126319:9992471:chr3_band59_s186000000_e187900000.fj 9126118790:16105743:chr3_band5_s13300000_e16400000.fj 9142224533:23723049:chr3_band60_s187900000_e192300000.fj 9165947582:28740659:chr3_band61_s192300000_e198022430.fj 9194688241:39101485:chr3_band6_s16400000_e23900000.fj 9233789726:13179037:chr3_band7_s23900000_e26400000.fj 9246968763:23659026:chr3_band8_s26400000_e30900000.fj 9270627789:6320874:chr3_band9_s30900000_e32100000.fj 9276948663:22624820:chr4_band0_s0_e4500000.fj 9299573483:19209706:chr4_band10_s44600000_e48200000.fj 9318783189:6384513:chr4_band11_s48200000_e50400000.fj 9325167702:4766253:chr4_band12_s50400000_e52700000.fj 9329933955:35018116:chr4_band13_s52700000_e59500000.fj 9364952071:38549974:chr4_band14_s59500000_e66600000.fj 9403502045:20373460:chr4_band15_s66600000_e70500000.fj 9423875505:29919881:chr4_band16_s70500000_e76300000.fj 9453795386:13493480:chr4_band17_s76300000_e78900000.fj 9467288866:18466490:chr4_band18_s78900000_e82400000.fj 9485755356:8860418:chr4_band19_s82400000_e84100000.fj 9494615774:7798021:chr4_band1_s4500000_e6000000.fj 9502413795:14575657:chr4_band20_s84100000_e86900000.fj 9516989452:5634479:chr4_band21_s86900000_e88000000.fj 9522623931:29718269:chr4_band22_s88000000_e93700000.fj 9552342200:7383995:chr4_band23_s93700000_e95100000.fj 9559726195:19715177:chr4_band24_s95100000_e98800000.fj 9579441372:11922350:chr4_band25_s98800000_e101100000.fj 9591363722:34698356:chr4_band26_s101100000_e107700000.fj 9626062078:33645974:chr4_band27_s107700000_e114100000.fj 9659708052:35587370:chr4_band28_s114100000_e120800000.fj 9695295422:15811642:chr4_band29_s120800000_e123800000.fj 9711107064:27146461:chr4_band2_s6000000_e11300000.fj 9738253525:26736354:chr4_band30_s123800000_e128800000.fj 9764989879:12053649:chr4_band31_s128800000_e131100000.fj 9777043528:45621870:chr4_band32_s131100000_e139500000.fj 9822665398:10457142:chr4_band33_s139500000_e141500000.fj 9833122540:27183032:chr4_band34_s141500000_e146800000.fj 9860305572:8901657:chr4_band35_s146800000_e148500000.fj 9869207229:13650247:chr4_band36_s148500000_e151100000.fj 9882857476:23802908:chr4_band37_s151100000_e155600000.fj 9906660384:33300872:chr4_band38_s155600000_e161800000.fj 9939961256:14822270:chr4_band39_s161800000_e164500000.fj 9954783526:20780182:chr4_band3_s11300000_e15200000.fj 9975563708:29757577:chr4_band40_s164500000_e170100000.fj 10005321285:9439391:chr4_band41_s170100000_e171900000.fj 10014760676:23890991:chr4_band42_s171900000_e176300000.fj 10038651667:6504378:chr4_band43_s176300000_e177500000.fj 10045156045:31429424:chr4_band44_s177500000_e183200000.fj 10076585469:20867286:chr4_band45_s183200000_e187100000.fj 10097452755:21542259:chr4_band46_s187100000_e191154276.fj 10118995014:13635330:chr4_band4_s15200000_e17800000.fj 10132630344:18645443:chr4_band5_s17800000_e21300000.fj 10151275787:33763872:chr4_band6_s21300000_e27700000.fj 10185039659:43935944:chr4_band7_s27700000_e35800000.fj 10228975603:28173344:chr4_band8_s35800000_e41200000.fj 10257148947:17960379:chr4_band9_s41200000_e44600000.fj 10275109326:23869100:chr5_band0_s0_e4500000.fj 10298978426:21475297:chr5_band10_s38400000_e42500000.fj 10320453723:18293853:chr5_band11_s42500000_e46100000.fj 10338747576:1514377:chr5_band12_s46100000_e48400000.fj 10340261953:8509364:chr5_band13_s48400000_e50700000.fj 10348771317:43017890:chr5_band14_s50700000_e58900000.fj 10391789207:20665117:chr5_band15_s58900000_e62900000.fj 10412454324:1591467:chr5_band16_s62900000_e63200000.fj 10414045791:18148759:chr5_band17_s63200000_e66700000.fj 10432194550:8856200:chr5_band18_s66700000_e68400000.fj 10441050750:20707621:chr5_band19_s68400000_e73300000.fj 10461758371:9656450:chr5_band1_s4500000_e6300000.fj 10471414821:18425621:chr5_band20_s73300000_e76900000.fj 10489840442:23196332:chr5_band21_s76900000_e81400000.fj 10513036774:7300891:chr5_band22_s81400000_e82800000.fj 10520337665:49892537:chr5_band23_s82800000_e92300000.fj 10570230202:30721980:chr5_band24_s92300000_e98200000.fj 10600952182:23888340:chr5_band25_s98200000_e102800000.fj 10624840522:9229611:chr5_band26_s102800000_e104500000.fj 10634070133:27421753:chr5_band27_s104500000_e109600000.fj 10661491886:9899436:chr5_band28_s109600000_e111500000.fj 10671391322:8406659:chr5_band29_s111500000_e113100000.fj 10679797981:18694996:chr5_band2_s6300000_e9800000.fj 10698492977:11028527:chr5_band30_s113100000_e115200000.fj 10709521504:32909679:chr5_band31_s115200000_e121400000.fj 10742431183:30963436:chr5_band32_s121400000_e127300000.fj 10773394619:17266919:chr5_band33_s127300000_e130600000.fj 10790661538:28998009:chr5_band34_s130600000_e136200000.fj 10819659547:16704607:chr5_band35_s136200000_e139500000.fj 10836364154:26045175:chr5_band36_s139500000_e144500000.fj 10862409329:27918575:chr5_band37_s144500000_e149800000.fj 10890327904:15050054:chr5_band38_s149800000_e152700000.fj 10905377958:15603577:chr5_band39_s152700000_e155700000.fj 10920981535:27716393:chr5_band3_s9800000_e15000000.fj 10948697928:22019757:chr5_band40_s155700000_e159900000.fj 10970717685:45797643:chr5_band41_s159900000_e168500000.fj 11016515328:22514380:chr5_band42_s168500000_e172800000.fj 11039029708:19253951:chr5_band43_s172800000_e176600000.fj 11058283659:21229495:chr5_band44_s176600000_e180915260.fj 11079513154:17559372:chr5_band4_s15000000_e18400000.fj 11097072526:25526673:chr5_band5_s18400000_e23300000.fj 11122599199:7096070:chr5_band6_s23300000_e24600000.fj 11129695269:23411851:chr5_band7_s24600000_e28900000.fj 11153107120:26119054:chr5_band8_s28900000_e33800000.fj 11179226174:23290349:chr5_band9_s33800000_e38400000.fj 11202516523:11854057:chr6_band0_s0_e2300000.fj 11214370580:8496414:chr6_band10_s30400000_e32100000.fj 11222866994:7220728:chr6_band11_s32100000_e33500000.fj 11230087722:15866348:chr6_band12_s33500000_e36600000.fj 11245954070:20565771:chr6_band13_s36600000_e40500000.fj 11266519841:29696078:chr6_band14_s40500000_e46200000.fj 11296215919:29661980:chr6_band15_s46200000_e51800000.fj 11325877899:5687860:chr6_band16_s51800000_e52900000.fj 11331565759:21802934:chr6_band17_s52900000_e57000000.fj 11353368693:8125890:chr6_band18_s57000000_e58700000.fj 11361494583:345265:chr6_band19_s58700000_e61000000.fj 11361839848:10003929:chr6_band1_s2300000_e4200000.fj 11371843777:8997133:chr6_band20_s61000000_e63300000.fj 11380840910:550060:chr6_band21_s63300000_e63400000.fj 11381390970:35514558:chr6_band22_s63400000_e70000000.fj 11416905528:30770003:chr6_band23_s70000000_e75900000.fj 11447675531:41661599:chr6_band24_s75900000_e83900000.fj 11489337130:5032680:chr6_band25_s83900000_e84900000.fj 11494369810:15730167:chr6_band26_s84900000_e88000000.fj 11510099977:26698981:chr6_band27_s88000000_e93100000.fj 11536798958:33870086:chr6_band28_s93100000_e99500000.fj 11570669044:5783371:chr6_band29_s99500000_e100600000.fj 11576452415:15201350:chr6_band2_s4200000_e7100000.fj 11591653765:26318508:chr6_band30_s100600000_e105500000.fj 11617972273:47367411:chr6_band31_s105500000_e114600000.fj 11665339684:19419515:chr6_band32_s114600000_e118300000.fj 11684759199:1079105:chr6_band33_s118300000_e118500000.fj 11685838304:40594325:chr6_band34_s118500000_e126100000.fj 11726432629:5183249:chr6_band35_s126100000_e127100000.fj 11731615878:17064012:chr6_band36_s127100000_e130300000.fj 11748679890:4703673:chr6_band37_s130300000_e131200000.fj 11753383563:20937849:chr6_band38_s131200000_e135200000.fj 11774321412:19768577:chr6_band39_s135200000_e139000000.fj 11794089989:18520799:chr6_band3_s7100000_e10600000.fj 11812610788:20084958:chr6_band40_s139000000_e142800000.fj 11832695746:14583555:chr6_band41_s142800000_e145600000.fj 11847279301:17888235:chr6_band42_s145600000_e149000000.fj 11865167536:18200150:chr6_band43_s149000000_e152500000.fj 11883367686:15899684:chr6_band44_s152500000_e155500000.fj 11899267370:28588964:chr6_band45_s155500000_e161000000.fj 11927856334:18688807:chr6_band46_s161000000_e164500000.fj 11946545141:34299518:chr6_band47_s164500000_e171115067.fj 11980844659:5187494:chr6_band4_s10600000_e11600000.fj 11986032153:9550459:chr6_band5_s11600000_e13400000.fj 11995582612:9425852:chr6_band6_s13400000_e15200000.fj 12005008464:52257569:chr6_band7_s15200000_e25200000.fj 12057266033:8929925:chr6_band8_s25200000_e27000000.fj 12066195958:17556391:chr6_band9_s27000000_e30400000.fj 12083752349:14247713:chr7_band0_s0_e2800000.fj 12098000062:11066306:chr7_band10_s35000000_e37200000.fj 12109066368:32087088:chr7_band11_s37200000_e43300000.fj 12141153456:10668222:chr7_band12_s43300000_e45400000.fj 12151821678:18626376:chr7_band13_s45400000_e49000000.fj 12170448054:7958919:chr7_band14_s49000000_e50500000.fj 12178406973:18713509:chr7_band15_s50500000_e54000000.fj 12197120482:18935900:chr7_band16_s54000000_e58000000.fj 12216056382:261032:chr7_band17_s58000000_e59900000.fj 12216317414:4289180:chr7_band18_s59900000_e61700000.fj 12220606594:22817045:chr7_band19_s61700000_e67000000.fj 12243423639:8953071:chr7_band1_s2800000_e4500000.fj 12252376710:26475183:chr7_band20_s67000000_e72200000.fj 12278851893:21260557:chr7_band21_s72200000_e77500000.fj 12300112450:47850592:chr7_band22_s77500000_e86400000.fj 12347963042:9284520:chr7_band23_s86400000_e88200000.fj 12357247562:15247848:chr7_band24_s88200000_e91100000.fj 12372495410:8580818:chr7_band25_s91100000_e92800000.fj 12381076228:26810427:chr7_band26_s92800000_e98000000.fj 12407886655:27671122:chr7_band27_s98000000_e103800000.fj 12435557777:3665630:chr7_band28_s103800000_e104500000.fj 12439223407:14940321:chr7_band29_s104500000_e107400000.fj 12454163728:12957633:chr7_band2_s4500000_e7300000.fj 12467121361:38098753:chr7_band30_s107400000_e114600000.fj 12505220114:14874016:chr7_band31_s114600000_e117400000.fj 12520094130:19901201:chr7_band32_s117400000_e121100000.fj 12539995331:14314479:chr7_band33_s121100000_e123800000.fj 12554309810:17691683:chr7_band34_s123800000_e127100000.fj 12572001493:10632852:chr7_band35_s127100000_e129200000.fj 12582634345:5806847:chr7_band36_s129200000_e130400000.fj 12588441192:11677084:chr7_band37_s130400000_e132600000.fj 12600118276:29615252:chr7_band38_s132600000_e138200000.fj 12629733528:24913008:chr7_band39_s138200000_e143100000.fj 12654646536:35077014:chr7_band3_s7300000_e13800000.fj 12689723550:23967238:chr7_band40_s143100000_e147900000.fj 12713690788:23454742:chr7_band41_s147900000_e152600000.fj 12737145530:12478502:chr7_band42_s152600000_e155100000.fj 12749624032:21624407:chr7_band43_s155100000_e159138663.fj 12771248439:14556948:chr7_band4_s13800000_e16500000.fj 12785805387:23503963:chr7_band5_s16500000_e20900000.fj 12809309350:23959972:chr7_band6_s20900000_e25500000.fj 12833269322:13023330:chr7_band7_s25500000_e28000000.fj 12846292652:4304022:chr7_band8_s28000000_e28800000.fj 12850596674:32133100:chr7_band9_s28800000_e35000000.fj 12882729774:11110146:chr8_band0_s0_e2200000.fj 12893839920:17322291:chr8_band10_s39700000_e43100000.fj 12911162211:3523440:chr8_band11_s43100000_e45600000.fj 12914685651:8688009:chr8_band12_s45600000_e48100000.fj 12923373660:21279449:chr8_band13_s48100000_e52200000.fj 12944653109:2084644:chr8_band14_s52200000_e52600000.fj 12946737753:14865421:chr8_band15_s52600000_e55500000.fj 12961603174:31742008:chr8_band16_s55500000_e61600000.fj 12993345182:3139055:chr8_band17_s61600000_e62200000.fj 12996484237:20025220:chr8_band18_s62200000_e66000000.fj 13016509457:10184650:chr8_band19_s66000000_e68000000.fj 13026694107:22530516:chr8_band1_s2200000_e6200000.fj 13049224623:13062826:chr8_band20_s68000000_e70500000.fj 13062287449:17612880:chr8_band21_s70500000_e73900000.fj 13079900329:23191965:chr8_band22_s73900000_e78300000.fj 13103092294:9532882:chr8_band23_s78300000_e80100000.fj 13112625176:23483810:chr8_band24_s80100000_e84600000.fj 13136108986:11316441:chr8_band25_s84600000_e86900000.fj 13147425427:33406608:chr8_band26_s86900000_e93300000.fj 13180832035:29555310:chr8_band27_s93300000_e99000000.fj 13210387345:13315728:chr8_band28_s99000000_e101600000.fj 13223703073:24175332:chr8_band29_s101600000_e106200000.fj 13247878405:30528779:chr8_band2_s6200000_e12700000.fj 13278407184:22764188:chr8_band30_s106200000_e110500000.fj 13301171372:8501850:chr8_band31_s110500000_e112100000.fj 13309673222:30459907:chr8_band32_s112100000_e117700000.fj 13340133129:7937153:chr8_band33_s117700000_e119200000.fj 13348070282:17300095:chr8_band34_s119200000_e122500000.fj 13365370377:25178211:chr8_band35_s122500000_e127300000.fj 13390548588:22053170:chr8_band36_s127300000_e131500000.fj 13412601758:26170716:chr8_band37_s131500000_e136400000.fj 13438772474:18915984:chr8_band38_s136400000_e139900000.fj 13457688458:33011109:chr8_band39_s139900000_e146364022.fj 13490699567:34615593:chr8_band3_s12700000_e19000000.fj 13525315160:22721686:chr8_band4_s19000000_e23300000.fj 13548036846:21858716:chr8_band5_s23300000_e27400000.fj 13569895562:7279298:chr8_band6_s27400000_e28800000.fj 13577174860:40036264:chr8_band7_s28800000_e36500000.fj 13617211124:9223086:chr8_band8_s36500000_e38300000.fj 13626434210:7285487:chr8_band9_s38300000_e39700000.fj 13633719697:11189873:chr9_band0_s0_e2200000.fj 13644909570:15398551:chr9_band10_s33200000_e36300000.fj 13660308121:10859291:chr9_band11_s36300000_e38400000.fj 13671167412:8098913:chr9_band12_s38400000_e41000000.fj 13679266325:7680539:chr9_band13_s41000000_e43600000.fj 13686946864:11204600:chr9_band14_s43600000_e47300000.fj 13698151464:54388:chr9_band15_s47300000_e49000000.fj 13698205852:3468752:chr9_band16_s49000000_e50700000.fj 13701674604:31370255:chr9_band17_s50700000_e65900000.fj 13733044859:8507254:chr9_band18_s65900000_e68700000.fj 13741552113:13175993:chr9_band19_s68700000_e72200000.fj 13754728106:12788834:chr9_band1_s2200000_e4600000.fj 13767516940:9460838:chr9_band20_s72200000_e74000000.fj 13776977778:27323515:chr9_band21_s74000000_e79200000.fj 13804301293:9840418:chr9_band22_s79200000_e81100000.fj 13814141711:15804284:chr9_band23_s81100000_e84100000.fj 13829945995:14087052:chr9_band24_s84100000_e86900000.fj 13844033047:17922209:chr9_band25_s86900000_e90400000.fj 13861955256:6951532:chr9_band26_s90400000_e91800000.fj 13868906788:10179374:chr9_band27_s91800000_e93900000.fj 13879086162:13769787:chr9_band28_s93900000_e96600000.fj 13892855949:13598284:chr9_band29_s96600000_e99300000.fj 13906454233:22891146:chr9_band2_s4600000_e9000000.fj 13929345379:16625559:chr9_band30_s99300000_e102600000.fj 13945970938:29592637:chr9_band31_s102600000_e108200000.fj 13975563575:16101070:chr9_band32_s108200000_e111300000.fj 13991664645:18810443:chr9_band33_s111300000_e114900000.fj 14010475088:14696084:chr9_band34_s114900000_e117700000.fj 14025171172:25738558:chr9_band35_s117700000_e122500000.fj 14050909730:17151669:chr9_band36_s122500000_e125800000.fj 14068061399:23282243:chr9_band37_s125800000_e130300000.fj 14091343642:15933520:chr9_band38_s130300000_e133500000.fj 14107277162:2539339:chr9_band39_s133500000_e134000000.fj 14109816501:28353605:chr9_band3_s9000000_e14200000.fj 14138170106:9895904:chr9_band40_s134000000_e135900000.fj 14148066010:7847526:chr9_band41_s135900000_e137400000.fj 14155913536:19420968:chr9_band42_s137400000_e141213431.fj 14175334504:12634378:chr9_band4_s14200000_e16600000.fj 14187968882:10157396:chr9_band5_s16600000_e18500000.fj 14198126278:7156443:chr9_band6_s18500000_e19900000.fj 14205282721:29952199:chr9_band7_s19900000_e25600000.fj 14235234920:12640911:chr9_band8_s25600000_e28000000.fj 14247875831:28005247:chr9_band9_s28000000_e33200000.fj 14275881078:61284:chrM_band0_s0_e16571.fj 14275942362:14544713:chrX_band0_s0_e4300000.fj 14290487075:24455427:chrX_band10_s37600000_e42400000.fj 14314942502:19979357:chrX_band11_s42400000_e46400000.fj 14334921859:15780934:chrX_band12_s46400000_e49800000.fj 14350702793:22068346:chrX_band13_s49800000_e54800000.fj 14372771139:15483950:chrX_band14_s54800000_e58100000.fj 14388255089:2245852:chrX_band15_s58100000_e60600000.fj 14390500941:8206011:chrX_band16_s60600000_e63000000.fj 14398706952:7456077:chrX_band17_s63000000_e64600000.fj 14406163029:15544112:chrX_band18_s64600000_e67800000.fj 14421707141:19435511:chrX_band19_s67800000_e71800000.fj 14441142652:8840055:chrX_band1_s4300000_e6000000.fj 14449982707:9325419:chrX_band20_s71800000_e73900000.fj 14459308126:9811498:chrX_band21_s73900000_e76000000.fj 14469119624:42415433:chrX_band22_s76000000_e84600000.fj 14511535057:8048575:chrX_band23_s84600000_e86200000.fj 14519583632:22647045:chrX_band24_s86200000_e91800000.fj 14542230677:7507452:chrX_band25_s91800000_e93500000.fj 14549738129:24532176:chrX_band26_s93500000_e98300000.fj 14574270305:20702445:chrX_band27_s98300000_e102600000.fj 14594972750:5393310:chrX_band28_s102600000_e103700000.fj 14600366060:25038697:chrX_band29_s103700000_e108700000.fj 14625404757:17528792:chrX_band2_s6000000_e9500000.fj 14642933549:39123936:chrX_band30_s108700000_e116500000.fj 14682057485:21530282:chrX_band31_s116500000_e120900000.fj 14703587767:39763257:chrX_band32_s120900000_e128700000.fj 14743351024:8637631:chrX_band33_s128700000_e130400000.fj 14751988655:16073438:chrX_band34_s130400000_e133600000.fj 14768062093:21768801:chrX_band35_s133600000_e138000000.fj 14789830894:11318859:chrX_band36_s138000000_e140300000.fj 14801149753:8828151:chrX_band37_s140300000_e142100000.fj
diff --git a/sdk/go/manifest/testdata/short_manifest b/sdk/go/manifest/testdata/short_manifest
deleted file mode 100644 (file)
index e8a0e43..0000000
+++ /dev/null
@@ -1 +0,0 @@
-. b746e3d2104645f2f64cd3cc69dd895d+15693477+E2866e643690156651c03d876e638e674dcd79475@5441920c 0:15693477:chr10_band0_s0_e3000000.fj
index a455f35399e4e0d67a632a2effdd07966f498b9e..bffe8f5925d49f8fd0f8041e2d2e0be9ef00ac3c 100644 (file)
@@ -68,12 +68,12 @@ Installing on Red Hat, AlmaLinux, and Rocky Linux
 
 Arvados publishes packages for RHEL 8 and distributions based on it. Note that these packages depend on, and will automatically enable, the Python 3.9 module. You can install the Python SDK package on any of these distributions by running the following commands::
 
-  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<EOF
+  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<'EOF'
   [arvados]
   name=Arvados
-  baseurl=http://rpm.arvados.org/CentOS/\$releasever/os/\$basearch/
+  baseurl=http://rpm.arvados.org/RHEL/$releasever/os/$basearch/
   gpgcheck=1
-  gpgkey=http://rpm.arvados.org/CentOS/RPM-GPG-KEY-arvados
+  gpgkey=http://rpm.arvados.org/RHEL/RPM-GPG-KEY-arvados
   EOF
   sudo dnf install python3-arvados-python-client
 
index c03db3c980c0d32fd93deac2088d8c034a772a0d..cf4ef6baa03de16de9784cf2e43f7ba1ada3ff1f 100755 (executable)
@@ -642,7 +642,7 @@ def copy_collection(obj_uuid, src, dst, args):
                 logger.debug("Getting block %s", word)
                 data = src_keep.get(word)
                 put_queue.put((word, data))
-            except e:
+            except Exception as e:
                 logger.error("Error getting block %s: %s", word, e)
                 transfer_error.append(e)
                 try:
@@ -680,7 +680,7 @@ def copy_collection(obj_uuid, src, dst, args):
                     bytes_written += loc.size
                     if progress_writer:
                         progress_writer.report(obj_uuid, bytes_written, bytes_expected)
-            except e:
+            except Exception as e:
                 logger.error("Error putting block %s (%s bytes): %s", blockhash, loc.size, e)
                 try:
                     # Drain the 'get' queue so we end early
index 8b1bbdfc8760861f49f04a90939e80b27b03a400..861d55871f3ff5a19b35876fdb079f258f1417bf 100644 (file)
@@ -8,67 +8,67 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (7.0.8.1)
-      actionpack (= 7.0.8.1)
-      activesupport (= 7.0.8.1)
+    actioncable (7.0.8.4)
+      actionpack (= 7.0.8.4)
+      activesupport (= 7.0.8.4)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
-    actionmailbox (7.0.8.1)
-      actionpack (= 7.0.8.1)
-      activejob (= 7.0.8.1)
-      activerecord (= 7.0.8.1)
-      activestorage (= 7.0.8.1)
-      activesupport (= 7.0.8.1)
+    actionmailbox (7.0.8.4)
+      actionpack (= 7.0.8.4)
+      activejob (= 7.0.8.4)
+      activerecord (= 7.0.8.4)
+      activestorage (= 7.0.8.4)
+      activesupport (= 7.0.8.4)
       mail (>= 2.7.1)
       net-imap
       net-pop
       net-smtp
-    actionmailer (7.0.8.1)
-      actionpack (= 7.0.8.1)
-      actionview (= 7.0.8.1)
-      activejob (= 7.0.8.1)
-      activesupport (= 7.0.8.1)
+    actionmailer (7.0.8.4)
+      actionpack (= 7.0.8.4)
+      actionview (= 7.0.8.4)
+      activejob (= 7.0.8.4)
+      activesupport (= 7.0.8.4)
       mail (~> 2.5, >= 2.5.4)
       net-imap
       net-pop
       net-smtp
       rails-dom-testing (~> 2.0)
-    actionpack (7.0.8.1)
-      actionview (= 7.0.8.1)
-      activesupport (= 7.0.8.1)
+    actionpack (7.0.8.4)
+      actionview (= 7.0.8.4)
+      activesupport (= 7.0.8.4)
       rack (~> 2.0, >= 2.2.4)
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.2.0)
-    actiontext (7.0.8.1)
-      actionpack (= 7.0.8.1)
-      activerecord (= 7.0.8.1)
-      activestorage (= 7.0.8.1)
-      activesupport (= 7.0.8.1)
+    actiontext (7.0.8.4)
+      actionpack (= 7.0.8.4)
+      activerecord (= 7.0.8.4)
+      activestorage (= 7.0.8.4)
+      activesupport (= 7.0.8.4)
       globalid (>= 0.6.0)
       nokogiri (>= 1.8.5)
-    actionview (7.0.8.1)
-      activesupport (= 7.0.8.1)
+    actionview (7.0.8.4)
+      activesupport (= 7.0.8.4)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.1, >= 1.2.0)
-    activejob (7.0.8.1)
-      activesupport (= 7.0.8.1)
+    activejob (7.0.8.4)
+      activesupport (= 7.0.8.4)
       globalid (>= 0.3.6)
-    activemodel (7.0.8.1)
-      activesupport (= 7.0.8.1)
-    activerecord (7.0.8.1)
-      activemodel (= 7.0.8.1)
-      activesupport (= 7.0.8.1)
-    activestorage (7.0.8.1)
-      actionpack (= 7.0.8.1)
-      activejob (= 7.0.8.1)
-      activerecord (= 7.0.8.1)
-      activesupport (= 7.0.8.1)
+    activemodel (7.0.8.4)
+      activesupport (= 7.0.8.4)
+    activerecord (7.0.8.4)
+      activemodel (= 7.0.8.4)
+      activesupport (= 7.0.8.4)
+    activestorage (7.0.8.4)
+      actionpack (= 7.0.8.4)
+      activejob (= 7.0.8.4)
+      activerecord (= 7.0.8.4)
+      activesupport (= 7.0.8.4)
       marcel (~> 1.0)
       mini_mime (>= 1.1.0)
-    activesupport (7.0.8.1)
+    activesupport (7.0.8.4)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (>= 1.6, < 2)
       minitest (>= 5.1)
@@ -105,13 +105,13 @@ GEM
       extlib (>= 0.9.15)
       multi_json (>= 1.0.0)
     base64 (0.2.0)
-    builder (3.2.4)
+    builder (3.3.0)
     byebug (11.1.3)
-    concurrent-ruby (1.2.3)
+    concurrent-ruby (1.3.3)
     crass (1.0.6)
     date (3.3.4)
     docile (1.4.0)
-    erubi (1.12.0)
+    erubi (1.13.0)
     extlib (0.9.16)
     factory_bot (6.2.1)
       activesupport (>= 5.0.0)
@@ -141,7 +141,7 @@ GEM
       os (>= 0.9, < 2.0)
       signet (>= 0.16, < 2.a)
     httpclient (2.8.3)
-    i18n (1.14.4)
+    i18n (1.14.5)
       concurrent-ruby (~> 1.0)
     jquery-rails (4.6.0)
       rails-dom-testing (>= 1, < 3)
@@ -169,15 +169,15 @@ GEM
       net-pop
       net-smtp
     marcel (1.0.4)
-    method_source (1.0.0)
+    method_source (1.1.0)
     mini_mime (1.1.5)
-    mini_portile2 (2.8.5)
+    mini_portile2 (2.8.7)
     minitest (5.10.3)
     mocha (2.1.0)
       ruby2_keywords (>= 0.0.5)
     multi_json (1.15.0)
     multipart-post (2.4.0)
-    net-imap (0.3.7)
+    net-imap (0.4.14)
       date
       net-protocol
     net-pop (0.1.2)
@@ -186,7 +186,7 @@ GEM
       timeout
     net-smtp (0.5.0)
       net-protocol
-    nio4r (2.7.1)
+    nio4r (2.7.3)
     nokogiri (1.15.6)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
@@ -199,24 +199,24 @@ GEM
     pg (1.5.4)
     power_assert (2.0.3)
     public_suffix (5.0.4)
-    racc (1.7.3)
+    racc (1.8.0)
     rack (2.2.9)
     rack-test (2.1.0)
       rack (>= 1.3)
-    rails (7.0.8.1)
-      actioncable (= 7.0.8.1)
-      actionmailbox (= 7.0.8.1)
-      actionmailer (= 7.0.8.1)
-      actionpack (= 7.0.8.1)
-      actiontext (= 7.0.8.1)
-      actionview (= 7.0.8.1)
-      activejob (= 7.0.8.1)
-      activemodel (= 7.0.8.1)
-      activerecord (= 7.0.8.1)
-      activestorage (= 7.0.8.1)
-      activesupport (= 7.0.8.1)
+    rails (7.0.8.4)
+      actioncable (= 7.0.8.4)
+      actionmailbox (= 7.0.8.4)
+      actionmailer (= 7.0.8.4)
+      actionpack (= 7.0.8.4)
+      actiontext (= 7.0.8.4)
+      actionview (= 7.0.8.4)
+      activejob (= 7.0.8.4)
+      activemodel (= 7.0.8.4)
+      activerecord (= 7.0.8.4)
+      activestorage (= 7.0.8.4)
+      activesupport (= 7.0.8.4)
       bundler (>= 1.15.0)
-      railties (= 7.0.8.1)
+      railties (= 7.0.8.4)
     rails-controller-testing (1.0.5)
       actionpack (>= 5.0.1.rc1)
       actionview (>= 5.0.1.rc1)
@@ -231,9 +231,9 @@ GEM
     rails-observers (0.1.5)
       activemodel (>= 4.0)
     rails-perftest (0.0.7)
-    railties (7.0.8.1)
-      actionpack (= 7.0.8.1)
-      activesupport (= 7.0.8.1)
+    railties (7.0.8.4)
+      actionpack (= 7.0.8.4)
+      activesupport (= 7.0.8.4)
       method_source
       rake (>= 12.2)
       thor (~> 1.0)
@@ -280,7 +280,7 @@ GEM
     websocket-driver (0.7.6)
       websocket-extensions (>= 0.1.0)
     websocket-extensions (0.1.5)
-    zeitwerk (2.6.13)
+    zeitwerk (2.6.16)
     zlib (3.1.0)
 
 PLATFORMS
index 11212e1b6910f42916251b27f0fcfe4a549bfadd..36839a1da0f55109d9a4e653be797f9231f1742b 100644 (file)
@@ -11,8 +11,6 @@ class Arvados::V1::GroupsController < ApplicationController
   skip_before_action :find_object_by_uuid, only: :shared
   skip_before_action :render_404_if_no_object, only: :shared
 
-  TRASHABLE_CLASSES = ['project']
-
   def self._index_requires_parameters
     (super rescue {}).
       merge({
@@ -101,15 +99,6 @@ class Arvados::V1::GroupsController < ApplicationController
     end
   end
 
-  def destroy
-    if !TRASHABLE_CLASSES.include?(@object.group_class)
-      @object.destroy
-      show
-    else
-      super # Calls destroy from TrashableController module
-    end
-  end
-
   def render_404_if_no_object
     if params[:action] == 'contents'
       if !params[:uuid]
index 7d20cf77fdcd555de70d54fa931767337b7a586b..d1222da32305af395f76adf6b51b19242a420920 100644 (file)
@@ -19,19 +19,29 @@ class SysController < ApplicationController
         in_batches(of: 15).
         update_all('is_trashed = true')
 
-      # Sweep trashed projects and their contents (as well as role
-      # groups that were trashed before #18340 when that was
-      # disallowed)
+      # Want to make sure the #update_trash hook on the Group class
+      # runs.  It does a couple of important things:
+      #
+      # - For projects, puts all the subprojects in the trashed_groups table.
+      #
+      # - For role groups, outbound permissions are deleted.
       Group.
-        where('delete_at is not null and delete_at < statement_timestamp()').each do |project|
-          delete_project_and_contents(project.uuid)
+        where("is_trashed = false and trash_at < statement_timestamp()").each do |grp|
+        grp.is_trashed = true
+        grp.save
       end
+
+      # Sweep groups and their contents that are ready to be deleted
       Group.
-        where('is_trashed = false and trash_at < statement_timestamp()').
-        update_all('is_trashed = true')
+        where('delete_at is not null and delete_at < statement_timestamp()').each do |group|
+          delete_project_and_contents(group.uuid)
+      end
 
       # Sweep expired tokens
       ActiveRecord::Base.connection.execute("DELETE from api_client_authorizations where expires_at <= statement_timestamp()")
+
+      # Sweep unused uuid_locks entries
+      ActiveRecord::Base.connection.execute("DELETE FROM uuid_locks WHERE uuid IN (SELECT uuid FROM uuid_locks FOR UPDATE SKIP LOCKED)")
     end
     head :no_content
   end
@@ -43,19 +53,21 @@ class SysController < ApplicationController
     if !p
       raise "can't sweep group '#{p_uuid}', it may not exist"
     end
-    # First delete sub projects
-    Group.where({group_class: 'project', owner_uuid: p_uuid}).each do |sub_project|
-      delete_project_and_contents(sub_project.uuid)
-    end
-    # Next, iterate over all tables which have owner_uuid fields, with some
-    # exceptions, and delete records owned by this project
-    skipped_classes = ['Group', 'User']
-    ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |klass|
-      if !skipped_classes.include?(klass.name) && klass.columns.collect(&:name).include?('owner_uuid')
-        klass.where({owner_uuid: p_uuid}).in_batches(of: 15).destroy_all
+    if p.group_class == 'project'
+      # First delete sub projects and owned filter groups
+      Group.where({owner_uuid: p_uuid}).each do |sub_project|
+        delete_project_and_contents(sub_project.uuid)
+      end
+      # Next, iterate over all tables which have owner_uuid fields, with some
+      # exceptions, and delete records owned by this project
+      skipped_classes = ['Group', 'User']
+      ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |klass|
+        if !skipped_classes.include?(klass.name) && klass.columns.collect(&:name).include?('owner_uuid')
+          klass.where({owner_uuid: p_uuid}).in_batches(of: 15).destroy_all
+        end
       end
     end
-    # Finally delete the project itself
+    # Finally delete the group itself
     p.destroy
   end
 end
index d159b73c94d7b3dde5bf5fa1560d2fa636845da3..6d30fe1bab0eb1a0ecde129972e9daa5b6986989 100644 (file)
@@ -49,6 +49,30 @@ class Group < ArvadosModel
     t.add :can_manage
   end
 
+  def default_delete_after_trash_interval
+    if self.group_class == 'role'
+      ActiveSupport::Duration.build(0)
+    else
+      super
+    end
+  end
+
+  def minimum_delete_after_trash_interval
+    if self.group_class == 'role'
+      ActiveSupport::Duration.build(0)
+    else
+      super
+    end
+  end
+
+  def validate_trash_and_delete_timing
+    if self.group_class == 'role' && delete_at && delete_at != trash_at
+      errors.add :delete_at, "must be == trash_at for role groups"
+    else
+      super
+    end
+  end
+
   # check if admins are allowed to make changes to the project, e.g. it
   # isn't trashed or frozen.
   def admin_change_permitted
@@ -171,10 +195,17 @@ with temptable as (select * from project_subtree_with_trash_at($1, LEAST($2, $3)
       [self.uuid,
        TrashedGroup.find_by_group_uuid(self.owner_uuid).andand.trash_at,
        self.trash_at])
+
     if frozen_descendants.any?
       raise ArgumentError.new("cannot trash project containing frozen project #{frozen_descendants[0]["uuid"]}")
     end
 
+    if self.trash_at and self.group_class == 'role'
+      # if this is a role group that is now in the trash, it loses all
+      # of its outgoing permissions.
+      Link.where(link_class: 'permission', tail_uuid: self.uuid).destroy_all
+    end
+
     ActiveRecord::Base.connection.exec_query(%{
 with temptable as (select * from project_subtree_with_trash_at($1, LEAST($2, $3)::timestamp)),
 
@@ -243,6 +274,7 @@ insert into frozen_groups (uuid) select uuid from temptable where is_frozen on c
   end
 
   def clear_permissions_trash_frozen
+    Link.where(link_class: 'permission', tail_uuid: self.uuid).destroy_all
     ComputedPermission.where(target_uuid: uuid).delete_all
     ActiveRecord::Base.connection.exec_delete(
       "delete from trashed_groups where group_uuid=$1",
diff --git a/services/api/db/migrate/20240618121312_create_uuid_locks.rb b/services/api/db/migrate/20240618121312_create_uuid_locks.rb
new file mode 100644 (file)
index 0000000..3c9c1c1
--- /dev/null
@@ -0,0 +1,12 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class CreateUuidLocks < ActiveRecord::Migration[7.0]
+  def change
+    create_table :uuid_locks, id: false do |t|
+      t.string :uuid, null: false, index: {unique: true}
+      t.integer :n, null: false, default: 0
+    end
+  end
+end
index 0abf9e04b5890f3f76f7e1eadac7a4b78bd676c4..9d3f0d235d090b99d898486e32ab99287a542af2 100644 (file)
@@ -1388,6 +1388,16 @@ CREATE SEQUENCE public.users_id_seq
 ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
 
 
+--
+-- Name: uuid_locks; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.uuid_locks (
+    uuid character varying NOT NULL,
+    n integer DEFAULT 0 NOT NULL
+);
+
+
 --
 -- Name: virtual_machines; Type: TABLE; Schema: public; Owner: -
 --
@@ -2851,6 +2861,13 @@ CREATE UNIQUE INDEX index_users_on_username ON public.users USING btree (usernam
 CREATE UNIQUE INDEX index_users_on_uuid ON public.users USING btree (uuid);
 
 
+--
+-- Name: index_uuid_locks_on_uuid; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE UNIQUE INDEX index_uuid_locks_on_uuid ON public.uuid_locks USING btree (uuid);
+
+
 --
 -- Name: index_virtual_machines_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
@@ -3328,4 +3345,5 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('20231013000000'),
 ('20240329173437'),
 ('20240402162733'),
-('20240604183200');
+('20240604183200'),
+('20240618121312');
index f2d4d7c051d5f45ed205eef0487dac2a9de2bd29..0702bd90e9087d43b5523c9393877cf3ec15130a 100644 (file)
@@ -31,6 +31,7 @@ module CanBeAnOwner
                       'repositories',
                       'specimens',
                       'traits',
+                      'uuid_locks',
                     ])
       klass = t.classify.constantize
       next unless klass and 'owner_uuid'.in?(klass.columns.collect(&:name))
index 50611c305ded5d1d166914b1066f6d348a2af51b..e2e7ceac6f5db52ab5751492862a92249ea66a31 100644 (file)
@@ -43,6 +43,14 @@ module Trashable
     true
   end
 
+  def default_delete_after_trash_interval
+    Rails.configuration.Collections.DefaultTrashLifetime
+  end
+
+  def minimum_delete_after_trash_interval
+    Rails.configuration.Collections.BlobSigningTTL
+  end
+
   def default_trash_interval
     if trash_at_changed? && !delete_at_changed?
       # If trash_at is updated without touching delete_at,
@@ -50,7 +58,7 @@ module Trashable
       if trash_at.nil?
         self.delete_at = nil
       else
-        self.delete_at = trash_at + Rails.configuration.Collections.DefaultTrashLifetime.seconds
+        self.delete_at = trash_at + self.default_delete_after_trash_interval
       end
     elsif !trash_at || !delete_at || trash_at > delete_at
       # Not trash, or bogus arguments? Just validate in
@@ -65,7 +73,7 @@ module Trashable
       earliest_delete = [
         @validation_timestamp,
         trash_at_was,
-      ].compact.min + Rails.configuration.Collections.BlobSigningTTL
+      ].compact.min + minimum_delete_after_trash_interval
 
       # The previous value of delete_at is also an upper bound on the
       # longest-lived permission token. For example, if TTL=14,
@@ -95,8 +103,7 @@ module TrashableController
     if !@object.is_trashed
       @object.update!(trash_at: db_current_time)
     end
-    earliest_delete = (@object.trash_at +
-                       Rails.configuration.Collections.BlobSigningTTL)
+    earliest_delete = (@object.trash_at + @object.minimum_delete_after_trash_interval)
     if @object.delete_at > earliest_delete
       @object.update!(delete_at: earliest_delete)
     end
@@ -111,17 +118,22 @@ module TrashableController
   end
 
   def untrash
-    if @object.is_trashed
-      @object.trash_at = nil
+    if !@object.is_trashed
+      raise ArvadosModel::InvalidStateTransitionError.new("Item is not trashed, cannot untrash")
+    end
 
-      if params[:ensure_unique_name]
-        @object.save_with_unique_name!
-      else
-        @object.save!
-      end
+    if db_current_time >= @object.delete_at
+      raise ArvadosModel::InvalidStateTransitionError.new("delete_at time has already passed, cannot untrash")
+    end
+
+    @object.trash_at = nil
+
+    if params[:ensure_unique_name]
+      @object.save_with_unique_name!
     else
-      raise ArvadosModel::InvalidStateTransitionError.new("Item is not trashed, cannot untrash")
+      @object.save!
     end
+
     show
   end
 
index 9034ac6ee7d2dd72928388b51b4461bff2814af8..bfe0aad36b42793251c8e3a28d12fa831cfeb266 100644 (file)
@@ -403,7 +403,7 @@ trashed_project:
   name: trashed project
   group_class: project
   trash_at: 2001-01-01T00:00:00Z
-  delete_at: 2008-03-01T00:00:00Z
+  delete_at: 2038-03-01T00:00:00Z
   is_trashed: true
   modified_at: 2001-01-01T00:00:00Z
 
@@ -434,3 +434,13 @@ trashed_on_next_sweep:
   delete_at: 2038-03-01T00:00:00Z
   is_trashed: false
   modified_at: 2001-01-01T00:00:00Z
+
+trashed_role_on_next_sweep:
+  uuid: zzzzz-j7d0g-soontobetrashd2
+  owner_uuid: zzzzz-tpzed-000000000000000
+  name: soon to be trashed role group
+  group_class: role
+  trash_at: 2001-01-01T00:00:00Z
+  delete_at: 2001-01-01T00:00:00Z
+  is_trashed: false
+  modified_at: 2001-01-01T00:00:00Z
index 61ad60451d088f15a554cf8661b28ed0f977def8..57824863bdbb2e36ac5f4409578d47ea966b2e53 100644 (file)
@@ -980,3 +980,17 @@ future_project_user_member_of_all_users_group:
   name: can_write
   head_uuid: zzzzz-j7d0g-fffffffffffffff
   properties: {}
+
+foo_file_readable_by_soon_to_be_trashed_role:
+  uuid: zzzzz-o0j2j-5s8ry7sn7bwxb7w
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2014-01-24 20:42:26 -0800
+  modified_by_client_uuid: zzzzz-ozdt8-000000000000000
+  modified_by_user_uuid: zzzzz-tpzed-000000000000000
+  modified_at: 2014-01-24 20:42:26 -0800
+  updated_at: 2014-01-24 20:42:26 -0800
+  tail_uuid: zzzzz-j7d0g-soontobetrashd2
+  link_class: permission
+  name: can_read
+  head_uuid: zzzzz-4zz18-znfnqtbbv4spc3w
+  properties: {}
index 52ed140bae06f248eed340d5c4a430b743b04144..c01fa97836f74e13da36e50159c07056bfb5b33f 100644 (file)
@@ -575,8 +575,8 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
             format: :json,
           }
       assert_response :success
-      # Should not be trashed
-      assert_nil Group.find_by_uuid(groups(grp).uuid)
+      # Should be trashed
+      assert Group.find_by_uuid(groups(grp).uuid).is_trashed
     end
   end
 
index ab304c1c7ce94dbd07b3e46981a2957e04093a95..c3f13cf4b8e575b7f4cc088e011585a6cbc53c11 100644 (file)
@@ -91,6 +91,17 @@ class SysControllerTest < ActionController::TestCase
     assert_not_empty Group.where('uuid=? and is_trashed=true', p.uuid)
   end
 
+  test "trash_sweep - role groups are deleted" do
+    p = groups(:trashed_role_on_next_sweep)
+    assert_empty Group.where('uuid=? and is_trashed=true', p.uuid)
+    assert_not_empty Link.where(uuid: links(:foo_file_readable_by_soon_to_be_trashed_role).uuid)
+    authorize_with :admin
+    post :trash_sweep
+    assert_response :success
+    assert_empty Group.where(uuid: p.uuid)
+    assert_empty Link.where(uuid: links(:foo_file_readable_by_soon_to_be_trashed_role).uuid)
+  end
+
   test "trash_sweep - delete projects and their contents" do
     g_foo = groups(:trashed_project)
     g_bar = groups(:trashed_subproject)
@@ -110,6 +121,8 @@ class SysControllerTest < ActionController::TestCase
     assert_not_empty ContainerRequest.where(uuid: cr.uuid)
 
     authorize_with :admin
+    Group.find_by_uuid(g_foo.uuid).update!(delete_at: Time.now - 1.second)
+
     post :trash_sweep
     assert_response :success
 
@@ -122,9 +135,40 @@ class SysControllerTest < ActionController::TestCase
     assert_equal user_nr_was, User.all.length
     assert_equal coll_nr_was-2,        # collection_in_trashed_subproject
                  Collection.all.length # & deleted_on_next_sweep collections
-    assert_equal group_nr_was, Group.where('group_class<>?', 'project').length
+    assert_equal group_nr_was-1,       # trashed_role_on_next_sweep
+                 Group.where('group_class<>?', 'project').length
     assert_equal project_nr_was-3, Group.where(group_class: 'project').length
     assert_equal cr_nr_was-1, ContainerRequest.all.length
   end
 
+  test "trash_sweep - delete unused uuid_locks" do
+    uuid_active = "zzzzz-zzzzz-uuidlockstest11"
+    uuid_inactive = "zzzzz-zzzzz-uuidlockstest00"
+
+    ready = Queue.new
+    insertsql = "INSERT INTO uuid_locks (uuid) VALUES ($1) ON CONFLICT (uuid) do UPDATE SET n = uuid_locks.n+1"
+    url = ENV["DATABASE_URL"].sub(/\?.*/, '')
+    Thread.new do
+      conn = PG::Connection.new(url)
+      conn.exec_params(insertsql, [uuid_active])
+      conn.exec_params(insertsql, [uuid_inactive])
+      conn.transaction do |conn|
+        conn.exec_params(insertsql, [uuid_active])
+        ready << true
+        # If we keep this transaction open while trash_sweep runs, the
+        # uuid_active row shouldn't get deleted.
+        sleep 10
+      rescue
+        # Unblock main thread
+        ready << false
+        raise
+      end
+    end
+    assert_equal true, ready.pop
+    authorize_with :admin
+    post :trash_sweep
+    rows = ActiveRecord::Base.connection.exec_query("SELECT uuid FROM uuid_locks ORDER BY uuid", "", []).rows
+    assert_includes(rows, [uuid_active], "row with active lock (still held by thread) should not have been deleted")
+    refute_includes(rows, [uuid_inactive], "row with inactive lock should have been deleted")
+  end
 end
index 9636a82011ffd1e7d0559fa47d2ec5fa41dbc518..a2cfbb6a194ef379cff634cd7d715a039685f177 100644 (file)
@@ -273,6 +273,119 @@ class PermissionsTest < ActionDispatch::IntegrationTest
     assert_response 404
   end
 
+  test "adding can_read links from group to collection, user to group, then trash group" do
+    # try to read collection as spectator
+    get "/arvados/v1/collections/#{collections(:foo_file).uuid}",
+      params: {:format => :json},
+      headers: auth(:spectator)
+    assert_response 404
+
+    # add permission for group to read collection
+    post "/arvados/v1/links",
+      params: {
+        :format => :json,
+        :link => {
+          tail_uuid: groups(:private_role).uuid,
+          link_class: 'permission',
+          name: 'can_read',
+          head_uuid: collections(:foo_file).uuid,
+          properties: {}
+        }
+      },
+      headers: auth(:admin)
+    assert_response :success
+
+    # try to read collection as spectator
+    get "/arvados/v1/collections/#{collections(:foo_file).uuid}",
+      params: {:format => :json},
+      headers: auth(:spectator)
+    assert_response 404
+
+    # add permission for spectator to read group
+    post "/arvados/v1/links",
+      params: {
+        :format => :json,
+        :link => {
+          tail_uuid: users(:spectator).uuid,
+          link_class: 'permission',
+          name: 'can_read',
+          head_uuid: groups(:private_role).uuid,
+          properties: {}
+        }
+      },
+      headers: auth(:admin)
+    u = json_response['uuid']
+    assert_response :success
+
+    # try to read collection as spectator
+    get "/arvados/v1/collections/#{collections(:foo_file).uuid}",
+      params: {:format => :json},
+      headers: auth(:spectator)
+    assert_response :success
+
+    # put the group in the trash, this should keep the group members
+    # but delete the permissions.
+    post "/arvados/v1/groups/#{groups(:private_role).uuid}/trash",
+      params: {:format => :json},
+      headers: auth(:admin)
+    assert_response :success
+
+    # try to read collection as spectator, should fail now
+    get "/arvados/v1/collections/#{collections(:foo_file).uuid}",
+      params: {:format => :json},
+      headers: auth(:spectator)
+    assert_response 404
+
+    # should not be able to grant permission to a trashed group
+    post "/arvados/v1/links",
+      params: {
+        :format => :json,
+        :link => {
+          tail_uuid: groups(:private_role).uuid,
+          link_class: 'permission',
+          name: 'can_read',
+          head_uuid: collections(:foo_file).uuid,
+          properties: {}
+        }
+      },
+      headers: auth(:admin)
+    assert_response 422
+
+    # can't take group out of the trash
+    post "/arvados/v1/groups/#{groups(:private_role).uuid}/untrash",
+      params: {:format => :json},
+      headers: auth(:admin)
+    assert_response 422
+
+    # when a role group is untrashed the permissions don't
+    # automatically come back
+    get "/arvados/v1/collections/#{collections(:foo_file).uuid}",
+      params: {:format => :json},
+      headers: auth(:spectator)
+    assert_response 404
+
+    # can't add permission for group to read collection either
+    post "/arvados/v1/links",
+      params: {
+        :format => :json,
+        :link => {
+          tail_uuid: groups(:private_role).uuid,
+          link_class: 'permission',
+          name: 'can_read',
+          head_uuid: collections(:foo_file).uuid,
+          properties: {}
+        }
+      },
+      headers: auth(:admin)
+    assert_response 422
+
+    # still can't read foo file
+    get "/arvados/v1/collections/#{collections(:foo_file).uuid}",
+      params: {:format => :json},
+      headers: auth(:spectator)
+    assert_response 404
+  end
+
   test "read-only group-admin cannot modify administered user" do
     put "/arvados/v1/users/#{users(:active).uuid}",
       params: {
index 54f2a4945b63f302e6cf96fae10cb59680a35e6e..0355dff66055b66f13ea296aeee19ca68dd5fb1f 100644 (file)
@@ -67,12 +67,12 @@ Installing on Red Hat, AlmaLinux, and Rocky Linux
 
 Arvados publishes packages for RHEL 8 and distributions based on it. Note that these packages depend on, and will automatically enable, the Python 3.9 module. You can install the Python SDK package on any of these distributions by running the following commands::
 
-  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<EOF
+  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<'EOF'
   [arvados]
   name=Arvados
-  baseurl=http://rpm.arvados.org/CentOS/\$releasever/os/\$basearch/
+  baseurl=http://rpm.arvados.org/RHEL/$releasever/os/$basearch/
   gpgcheck=1
-  gpgkey=http://rpm.arvados.org/CentOS/RPM-GPG-KEY-arvados
+  gpgkey=http://rpm.arvados.org/RHEL/RPM-GPG-KEY-arvados
   EOF
   sudo dnf install python3-arvados-fuse
 
index 8d06ea29feccfcbb9667811d9cae2a64c9962fba..816ebd8708f2970a0d38b196d4145b3c01778a5f 100644 (file)
@@ -19,6 +19,7 @@ import { Provider } from "react-redux";
 import { FilterBuilder } from 'services/api/filter-builder';
 import { ProcessStatusFilter, buildProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
 import {act} from "react-dom/test-utils";
+import { updateResources } from 'store/resources/resources-actions';
 
 configure({ adapter: new Adapter() });
 
@@ -61,14 +62,17 @@ describe("<SubprocessProgressBar />", () => {
 
     it("requests subprocess progress stats for stopped processes and displays progress", async () => {
         // when
+        const containerRequest = {
+            uuid: 'zzzzz-xvhdp-000000000000000',
+            containerUuid: 'zzzzz-dz642-000000000000000',
+        };
         const process = {
             container: {
                 state: ContainerState.COMPLETE,
             },
-            containerRequest: {
-                containerUuid: 'zzzzz-dz642-000000000000000',
-            },
+            containerRequest: containerRequest,
         } as Process;
+        await store.dispatch(updateResources([containerRequest, process]));
 
         statusResponse = {
             [ProcessStatusFilter.COMPLETED]: 100,
@@ -124,14 +128,17 @@ describe("<SubprocessProgressBar />", () => {
     });
 
     it("dislays correct progress bar widths with different values", async () => {
+        const containerRequest = {
+            uuid: 'zzzzz-xvhdp-000000000000001',
+            containerUuid: 'zzzzz-dz642-000000000000001',
+        };
         const process = {
             container: {
                 state: ContainerState.COMPLETE,
             },
-            containerRequest: {
-                containerUuid: 'zzzzz-dz642-000000000000001',
-            },
+            containerRequest: containerRequest,
         } as Process;
+        await store.dispatch(updateResources([containerRequest, process]));
 
         statusResponse = {
             [ProcessStatusFilter.COMPLETED]: 50,
index 78df83f6ca86f39deeb98b39ac9050e99707d1ad..27a1c7b5e6241ea13bd3016745dced615aefd6c1 100644 (file)
@@ -6,10 +6,10 @@ import React, { useEffect, useState } from "react";
 import { StyleRulesCallback, Tooltip, WithStyles, withStyles } from "@material-ui/core";
 import { CProgressStacked, CProgress } from '@coreui/react';
 import { useAsyncInterval } from "common/use-async-interval";
-import { Process } from "store/processes/process";
+import { Process, isProcessRunning } from "store/processes/process";
 import { connect } from "react-redux";
 import { Dispatch } from "redux";
-import { fetchProcessProgressBarStatus } from "store/subprocess-panel/subprocess-panel-actions";
+import { fetchProcessProgressBarStatus, isProcess } from "store/subprocess-panel/subprocess-panel-actions";
 import { ProcessStatusFilter, serializeOnlyProcessTypeFilters } from "store/resource-type-filters/resource-type-filters";
 import { ProjectResource } from "models/project";
 import { getDataExplorer } from "store/data-explorer/data-explorer-reducer";
@@ -40,7 +40,7 @@ export interface ProgressBarDataProps {
 }
 
 export interface ProgressBarActionProps {
-    fetchProcessProgressBarStatus: (parentResource: Process | ProjectResource, typeFilter?: string) => Promise<ProgressBarStatus | undefined>;
+    fetchProcessProgressBarStatus: (parentResourceUuid: string, typeFilter?: string) => Promise<ProgressBarStatus | undefined>;
 }
 
 type ProgressBarProps = ProgressBarDataProps & ProgressBarActionProps & WithStyles<CssRules>;
@@ -54,7 +54,7 @@ export type ProgressBarCounts = {
 
 export type ProgressBarStatus = {
     counts: ProgressBarCounts;
-    isRunning: boolean;
+    shouldPollProject: boolean;
 };
 
 const mapStateToProps = (state: RootState, props: ProgressBarDataProps) => {
@@ -70,8 +70,8 @@ const mapStateToProps = (state: RootState, props: ProgressBarDataProps) => {
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ProgressBarActionProps => ({
-    fetchProcessProgressBarStatus: (parentResource: Process | ProjectResource, typeFilter?: string) => {
-        return dispatch<any>(fetchProcessProgressBarStatus(parentResource, typeFilter));
+    fetchProcessProgressBarStatus: (parentResourceUuid: string, typeFilter?: string) => {
+        return dispatch<any>(fetchProcessProgressBarStatus(parentResourceUuid, typeFilter));
     },
 });
 
@@ -79,31 +79,54 @@ export const SubprocessProgressBar = connect(mapStateToProps, mapDispatchToProps
     ({ parentResource, typeFilter, classes, fetchProcessProgressBarStatus }: ProgressBarProps) => {
 
         const [progressCounts, setProgressData] = useState<ProgressBarCounts | undefined>(undefined);
-        const [isRunning, setIsRunning] = useState<boolean>(false);
-
+        const [shouldPollProject, setShouldPollProject] = useState<boolean>(false);
+        const shouldPollProcess = isProcess(parentResource) ? isProcessRunning(parentResource) : false;
+
+        // Should polling be active based on container status
+        // or result of aggregated project process contents
+        const shouldPoll = shouldPollProject || shouldPollProcess;
+
+        const parentUuid = parentResource
+            ? isProcess(parentResource)
+                ? parentResource.containerRequest.uuid
+                : parentResource.uuid
+            : "";
+
+        // Runs periodically whenever polling should be happeing
+        // Either when the workflow is running (shouldPollProcess) or when the
+        //   project contains steps in an active state (shouldPollProject)
         useAsyncInterval(async () => {
-            if (parentResource) {
-                fetchProcessProgressBarStatus(parentResource, typeFilter)
+            if (parentUuid) {
+                fetchProcessProgressBarStatus(parentUuid, typeFilter)
                     .then(result => {
                         if (result) {
                             setProgressData(result.counts);
-                            setIsRunning(result.isRunning);
+                            setShouldPollProject(result.shouldPollProject);
                         }
                     });
             }
-        }, isRunning ? 5000 : null);
-
+        }, shouldPoll ? 5000 : null);
+
+        // Runs fetch on first load for processes and projects, except when
+        //   process is running since polling will be enabled by shouldPoll.
+        // Project polling starts false so this is still needed for project
+        //   initial load to set shouldPollProject and kick off shouldPoll
+        // Watches shouldPollProcess but not shouldPollProject
+        //   * This runs a final fetch when process ends & is updated through
+        //     websocket / store
+        //   * We ignore shouldPollProject entirely since it changes to false
+        //     as a result of a fetch so the data is already up to date
         useEffect(() => {
-            if (!isRunning && parentResource) {
-                fetchProcessProgressBarStatus(parentResource, typeFilter)
+            if (!shouldPollProcess && parentUuid) {
+                fetchProcessProgressBarStatus(parentUuid, typeFilter)
                     .then(result => {
                         if (result) {
                             setProgressData(result.counts);
-                            setIsRunning(result.isRunning);
+                            setShouldPollProject(result.shouldPollProject);
                         }
                     });
             }
-        }, [fetchProcessProgressBarStatus, isRunning, parentResource, typeFilter]);
+        }, [fetchProcessProgressBarStatus, shouldPollProcess, parentUuid, typeFilter]);
 
         let tooltip = "";
         if (progressCounts) {
index 4c31fa4e94b3878631ecadda72b0b832c3e97298..7a640cc20ac7ea6a2000a56c92589c9389a4b004 100644 (file)
@@ -25,6 +25,7 @@ import { resourceIsFrozen } from "common/frozen-resources";
 import { ProjectResource } from "models/project";
 import { getProcess } from "store/processes/process";
 import { filterCollectionFilesBySelection } from "store/collection-panel/collection-panel-files/collection-panel-files-state";
+import { selectOne, deselectAllOthers } from "store/multiselect/multiselect-actions";
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition; resource: ContextMenuResource }>(),
@@ -56,6 +57,8 @@ export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.n
 
 export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) => (dispatch: Dispatch) => {
     event.preventDefault();
+    dispatch<any>(selectOne(resource.uuid));
+    dispatch<any>(deselectAllOthers(resource.uuid));
     const { left, top } = event.currentTarget.getBoundingClientRect();
     dispatch(
         contextMenuActions.OPEN_CONTEXT_MENU({
index a3d5c68b62d7f381e5fe8103ded24bae57f945f7..e44c15ba242a6def21c380e272bbb9517e51bf7d 100644 (file)
@@ -9,8 +9,11 @@ import { bindDataExplorerActions } from 'store/data-explorer/data-explorer-actio
 import { FilterBuilder, joinFilters } from 'services/api/filter-builder';
 import { ProgressBarStatus, ProgressBarCounts } from 'components/subprocess-progress-bar/subprocess-progress-bar';
 import { ProcessStatusFilter, buildProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
-import { Process, isProcessRunning } from 'store/processes/process';
+import { Process } from 'store/processes/process';
 import { ProjectResource } from 'models/project';
+import { getResource } from 'store/resources/resources';
+import { ContainerRequestResource } from 'models/container-request';
+import { Resource } from 'models/resource';
 
 export const SUBPROCESS_PANEL_ID = "subprocessPanel";
 export const SUBPROCESS_ATTRIBUTES_DIALOG = 'subprocessAttributesDialog';
@@ -50,12 +53,29 @@ type ProgressBarStatusPair = {
     processStatus: ProcessStatusFilter;
 };
 
-const isProcess = (resource: Process | ProjectResource | undefined): resource is Process => {
+/**
+ * Type guard to distinguish Processes from other Resources
+ * @param resource The item to check
+ * @returns if the resource is a Process
+ */
+export const isProcess = <T extends Resource>(resource: T | Process | undefined): resource is Process => {
     return !!resource && 'containerRequest' in resource;
 };
 
-export const fetchProcessProgressBarStatus = (parentResource: Process | ProjectResource, typeFilter?: string) =>
+/**
+ * Type guard to distinguish ContainerRequestResources from Resources
+ * @param resource The item to check
+ * @returns if the resource is a ContainerRequestResource
+ */
+const isContainerRequest = <T extends Resource>(resource: T | ContainerRequestResource | undefined): resource is ContainerRequestResource => {
+    return !!resource && 'containerUuid' in resource;
+};
+
+export const fetchProcessProgressBarStatus = (parentResourceUuid: string, typeFilter?: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<ProgressBarStatus | undefined> => {
+        const resources = getState().resources;
+        const parentResource = getResource<ProjectResource | ContainerRequestResource>(parentResourceUuid)(resources);
+
         const requestContainerStatusCount = async (fb: FilterBuilder) => {
             return await services.containerRequestService.list({
                 limit: 0,
@@ -64,18 +84,21 @@ export const fetchProcessProgressBarStatus = (parentResource: Process | ProjectR
             });
         }
 
-        let baseFilter = "";
-        if (isProcess(parentResource)) {
-            baseFilter = new FilterBuilder().addEqual('requesting_container_uuid', parentResource.containerRequest.containerUuid).getFilters();
-        } else {
+        let baseFilter: string = "";
+        if (isContainerRequest(parentResource) && parentResource.containerUuid) {
+            // Prevent CR without containerUuid from generating baseFilter
+            baseFilter = new FilterBuilder().addEqual('requesting_container_uuid', parentResource.containerUuid).getFilters();
+        } else if (parentResource && !isContainerRequest(parentResource)) {
+            // isCR type narrowing needed since CR without container may fall through
             baseFilter = new FilterBuilder().addEqual('owner_uuid', parentResource.uuid).getFilters();
         }
 
-        if (typeFilter) {
-            baseFilter = joinFilters(baseFilter, typeFilter);
-        }
+        if (parentResource && baseFilter) {
+            // Add type filters from consumers that want to sync progress stats with filters
+            if (typeFilter) {
+                baseFilter = joinFilters(baseFilter, typeFilter);
+            }
 
-        if (baseFilter) {
             try {
                 // Create return object
                 let result: ProgressBarCounts = {
@@ -103,17 +126,23 @@ export const fetchProcessProgressBarStatus = (parentResource: Process | ProjectR
                     result[singleResult.status] += singleResult.count;
                 });
 
-                let isRunning = result[ProcessStatusFilter.RUNNING] + result[ProcessStatusFilter.QUEUED] > 0;
-
-                if (isProcess(parentResource)) {
-                    isRunning = isProcessRunning(parentResource);
-                }
-
-                return {counts: result, isRunning};
+                // CR polling is handled in progress bar based on store updates
+                // This bool triggers polling without causing a final fetch when disabled
+                // The shouldPoll logic here differs slightly from shouldPollProcess:
+                //   * Process gets websocket updates through the store so using isProcessRunning
+                //     ignores Queued
+                //   * In projects, we get no websocket updates on CR state changes so we treat
+                //     Queued processes as running in order to let polling keep us up to date
+                //     when anything transitions to Running. This also means that a project with
+                //     CRs in a stopped state won't start polling if CRs are started elsewhere
+                const shouldPollProject = isContainerRequest(parentResource)
+                    ? false
+                    : (result[ProcessStatusFilter.RUNNING] + result[ProcessStatusFilter.QUEUED]) > 0;
+
+                return {counts: result, shouldPollProject};
             } catch (e) {
                 return undefined;
             }
-        } else {
-            return undefined;
         }
+        return undefined;
     };
index 8ef968eea9a4025eb10f3f349001898b42be15d8..8098262ec12e0f0a76ce5ab0c6d482b69714a27b 100644 (file)
@@ -50,8 +50,8 @@ export const copyToClipboardMenuAction = {
 export const viewDetailsAction = {
     icon: DetailsIcon,
     name: ContextMenuActionNames.VIEW_DETAILS,
-    execute: dispatch => {
-        dispatch(toggleDetailsPanel());
+    execute: (dispatch, resources) => {
+        dispatch(toggleDetailsPanel(resources[0].uuid));
     },
 };
 
index 99d365df33f2c25b6ee595a022345e0bd2139bda..4b923d44de439d884a1dacb19a374780d88922f7 100644 (file)
@@ -65,12 +65,12 @@ Installing on Red Hat, AlmaLinux, and Rocky Linux
 
 Arvados publishes packages for RHEL 8 and distributions based on it. Note that these packages depend on, and will automatically enable, the Python 3.9 module. You can install the Python SDK package on any of these distributions by running the following commands::
 
-  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<EOF
+  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<'EOF'
   [arvados]
   name=Arvados
-  baseurl=http://rpm.arvados.org/CentOS/\$releasever/os/\$basearch/
+  baseurl=http://rpm.arvados.org/RHEL/$releasever/os/$basearch/
   gpgcheck=1
-  gpgkey=http://rpm.arvados.org/CentOS/RPM-GPG-KEY-arvados
+  gpgkey=http://rpm.arvados.org/RHEL/RPM-GPG-KEY-arvados
   EOF
   sudo dnf install python3-crunchstat-summary
 
diff --git a/tools/salt-install/config_examples/multi_host/aws/pillars/postgresql_external.sls b/tools/salt-install/config_examples/multi_host/aws/pillars/postgresql_external.sls
new file mode 100644 (file)
index 0000000..2303f17
--- /dev/null
@@ -0,0 +1,11 @@
+---
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+postgresql_external_service:
+  db_host: "__DATABASE_EXTERNAL_SERVICE_HOST_OR_IP__"
+  db_port: 5432
+  db_name: "__DATABASE_NAME__"
+  db_user: "__DATABASE_USER__"
+  db_password: "__DATABASE_PASSWORD__"
diff --git a/tools/salt-install/config_examples/multi_host/aws/states/postgresql_external.sls b/tools/salt-install/config_examples/multi_host/aws/states/postgresql_external.sls
new file mode 100644 (file)
index 0000000..aaf5b6c
--- /dev/null
@@ -0,0 +1,19 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+{%- set pg_svc = pillar.get('postgresql_external_service', {}) %}
+
+{%- if pg_svc %}
+__CLUSTER___external_trgm_extension:
+  postgres_extension.present:
+    - name: pg_trgm
+    - if_not_exists: true
+    - schema: public
+    - db_host: {{ pg_svc.db_host }}
+    - db_port: 5432
+    - db_user: {{ pg_svc.db_user }}
+    - db_password: {{ pg_svc.db_password }}
+    - require:
+      - pkg: postgresql-client-libs
+{%- endif %}
\ No newline at end of file
index 36e87cca91f1e774cc20193409815cc58f5f4c9a..2a0460c64847749ff80bb4cb059034136cabd73a 100755 (executable)
@@ -269,8 +269,16 @@ terraform)
   logfile=terraform-$(date -Iseconds).log
   (cd terraform/vpc && terraform apply -auto-approve) 2>&1 | tee -a $logfile
   (cd terraform/data-storage && terraform apply -auto-approve) 2>&1 | tee -a $logfile
-  (cd terraform/services && terraform apply -auto-approve) 2>&1 | grep -v letsencrypt_iam_secret_access_key | tee -a $logfile
-  (cd terraform/services && echo -n 'letsencrypt_iam_secret_access_key = ' && terraform output letsencrypt_iam_secret_access_key) 2>&1 | tee -a $logfile
+  (cd terraform/services && \
+    terraform apply -auto-approve) 2>&1 | \
+    grep -v letsencrypt_iam_secret_access_key | \
+    grep -v database_password | \
+    tee -a $logfile
+  (cd terraform/services && \
+    echo -n 'letsencrypt_iam_secret_access_key = ' && \
+    terraform output letsencrypt_iam_secret_access_key && \
+    echo -n 'database_password = ' && \
+    terraform output database_password) 2>&1 | tee -a $logfile
   ;;
 
 terraform-destroy)
index a57164276e28fd21a49b2fbc31801734b0117462..af48c367c2b0f9096047ad0731ae5ec655d454d4 100755 (executable)
@@ -742,6 +742,10 @@ else
   for R in ${ROLES:-}; do
     case "${R}" in
       "database")
+        # Skip if using an external service
+        if [[ "${DATABASE_EXTERNAL_SERVICE_HOST_OR_IP:-}" != "" ]]; then
+          continue
+        fi
         # States
         grep -q "\- postgres$" ${STATES_TOP} || echo "    - postgres" >> ${STATES_TOP}
         grep -q "extra.prometheus_pg_exporter" ${STATES_TOP} || echo "    - extra.prometheus_pg_exporter" >> ${STATES_TOP}
@@ -859,6 +863,9 @@ else
         fi
         echo "    - extra.passenger_rvm" >> ${STATES_TOP}
         grep -q "^    - postgres\\.client$" ${STATES_TOP} || echo "    - postgres.client" >> ${STATES_TOP}
+        if [[ "${DATABASE_EXTERNAL_SERVICE_HOST_OR_IP:-}" != "" ]]; then
+          grep -q "    - extra.postgresql_external" ${STATES_TOP} || echo "    - extra.postgresql_external" >> ${STATES_TOP}
+        fi
 
         ### If we don't install and run LE before arvados-api-server, it fails and breaks everything
         ### after it. So we add this here as we are, after all, sharing the host for api and controller
@@ -886,6 +893,10 @@ else
         grep -q "nginx_api_configuration" ${PILLARS_TOP} || echo "    - nginx_api_configuration" >> ${PILLARS_TOP}
         grep -q "nginx_controller_configuration" ${PILLARS_TOP} || echo "    - nginx_controller_configuration" >> ${PILLARS_TOP}
 
+        if [[ "${DATABASE_EXTERNAL_SERVICE_HOST_OR_IP:-}" != "" ]]; then
+          grep -q "    - postgresql_external" ${PILLARS_TOP} || echo "    - postgresql_external" >> ${PILLARS_TOP}
+        fi
+
         if [ "${ENABLE_BALANCER}" == "no" ]; then
           if [ "${SSL_MODE}" = "lets-encrypt" ]; then
             if [ "${USE_LETSENCRYPT_ROUTE53}" = "yes" ]; then
index 807bd7d01f14c663e51f2fda0be86ccd92ecd332..923b948baaf3a8e6670eef6c284e700f6d456256 100644 (file)
@@ -25,7 +25,18 @@ locals {
   }
   private_subnet_id = data.terraform_remote_state.vpc.outputs.private_subnet_id
   public_subnet_id = data.terraform_remote_state.vpc.outputs.public_subnet_id
+  additional_rds_subnet_id = data.terraform_remote_state.vpc.outputs.additional_rds_subnet_id
   arvados_sg_id = data.terraform_remote_state.vpc.outputs.arvados_sg_id
   eip_id = data.terraform_remote_state.vpc.outputs.eip_id
   keepstore_iam_role_name = data.terraform_remote_state.data-storage.outputs.keepstore_iam_role_name
+  use_rds = (var.use_rds && data.terraform_remote_state.vpc.outputs.use_rds)
+  rds_username = var.rds_username != "" ? var.rds_username : "${local.cluster_name}_arvados"
+  rds_password = var.rds_password != "" ? var.rds_password : one(random_string.default_rds_password[*].result)
+  rds_allocated_storage = var.rds_allocated_storage
+  rds_max_allocated_storage = max(var.rds_max_allocated_storage, var.rds_allocated_storage)
+  rds_instance_type = var.rds_instance_type
+  rds_backup_retention_period = var.rds_backup_retention_period
+  rds_backup_before_deletion = var.rds_backup_before_deletion
+  rds_final_backup_name = var.rds_final_backup_name != "" ? var.rds_final_backup_name : "arvados-${local.cluster_name}-db-final-snapshot"
+  rds_postgresql_version = var.rds_postgresql_version
 }
index 06564edad6cb3efd6c82f67b2874b300def493d3..6e51535abd595eb231cd2fb5bbe96aebc551f5e9 100644 (file)
@@ -22,6 +22,14 @@ provider "aws" {
   }
 }
 
+provider "random" {}
+
+resource "random_string" "default_rds_password" {
+  count = (local.use_rds && var.rds_password == "") ? 1 : 0
+  length  = 32
+  special = false
+}
+
 resource "aws_iam_instance_profile" "keepstore_instance_profile" {
   name = "${local.cluster_name}-keepstore-00-iam-role"
   role = data.terraform_remote_state.data-storage.outputs.keepstore_iam_role_name
@@ -82,6 +90,44 @@ resource "aws_instance" "arvados_service" {
   }
 }
 
+resource "aws_db_subnet_group" "arvados_db_subnet_group" {
+  count = local.use_rds ? 1 : 0
+  name       = "${local.cluster_name}_db_subnet_group"
+  subnet_ids = [local.private_subnet_id, local.additional_rds_subnet_id]
+}
+
+resource "aws_db_instance" "postgresql_service" {
+  count = local.use_rds ? 1 : 0
+  allocated_storage = local.rds_allocated_storage
+  max_allocated_storage = local.rds_max_allocated_storage
+  engine = "postgres"
+  engine_version = local.rds_postgresql_version
+  instance_class = local.rds_instance_type
+  db_name = "${local.cluster_name}_arvados"
+  username = local.rds_username
+  password = local.rds_password
+  skip_final_snapshot  = !local.rds_backup_before_deletion
+  final_snapshot_identifier = local.rds_final_backup_name
+
+  vpc_security_group_ids = [local.arvados_sg_id]
+  db_subnet_group_name = aws_db_subnet_group.arvados_db_subnet_group[0].name
+
+  backup_retention_period = local.rds_backup_retention_period
+  publicly_accessible = false
+  storage_encrypted = true
+  multi_az = false
+
+  lifecycle {
+    ignore_changes = [
+      username,
+    ]
+  }
+
+  tags = {
+    Name = "${local.cluster_name}_postgresql_service"
+  }
+}
+
 resource "aws_iam_policy" "compute_node_ebs_autoscaler" {
   name = "${local.cluster_name}_compute_node_ebs_autoscaler"
   policy = jsonencode({
index d0f9268ca2fcfacfa53e8ac0834d3702c4a74fc0..9f0dd1697eb9e0477d21db104453615c30391290 100644 (file)
@@ -59,4 +59,25 @@ output "region_name" {
 
 output "ssl_password_secret_name" {
   value = aws_secretsmanager_secret.ssl_password_secret.name
-}
\ No newline at end of file
+}
+
+output "database_address" {
+  value = one(aws_db_instance.postgresql_service[*].address)
+}
+
+output "database_name" {
+  value = one(aws_db_instance.postgresql_service[*].db_name)
+}
+
+output "database_username" {
+  value = one(aws_db_instance.postgresql_service[*].username)
+}
+
+output "database_password" {
+  value = one(aws_db_instance.postgresql_service[*].password)
+  sensitive = true
+}
+
+output "database_version" {
+  value = one(aws_db_instance.postgresql_service[*].engine_version_actual)
+}
index 965153756052ba11c010b5608ecc529b0bbfad6e..a11dde20cc5f130c8d99f685dc9138be516d0363 100644 (file)
 #   controller = 300
 # }
 
+# Use an RDS instance for database. For this to work, make sure to also set
+# 'use_rds' to true in '../vpc/terraform.tfvars'.
+# use_rds = true
+#
+# Provide custom values if needed.
+# rds_username = ""
+# rds_password = ""
+# rds_instance_type = "db.m5.xlarge"
+# rds_postgresql_version = "16.3"
+# rds_allocated_storage = 200
+# rds_max_allocated_storage = 1000
+# rds_backup_retention_period = 30
+# rds_backup_before_deletion = false
+# rds_final_backup_name = ""
+
 # AWS secret's name which holds the SSL certificate private key's password.
 # Default: "arvados-ssl-privkey-password"
 # ssl_password_secret_name_suffix = "some-name-suffix"
index 7e5d9056d41d9a579845bbe6fedb0b4531ad5cb3..1f1cca050c1b2ab786f61d65d969e7fb1e738f92 100644 (file)
@@ -41,4 +41,68 @@ variable "instance_ami" {
   description = "The EC2 instance AMI to use on the nodes"
   type = string
   default = ""
-}
\ No newline at end of file
+}
+
+variable "use_rds" {
+  description = "Enable to create an RDS instance as the cluster's database service"
+  type = bool
+  default = false
+}
+
+variable "rds_username" {
+  description = "RDS instance's username. Default: <cluster_name>_arvados"
+  type = string
+  default = ""
+}
+
+variable "rds_password" {
+  description = "RDS instance's password. Default: randomly-generated 32 chars"
+  type = string
+  default = ""
+}
+
+variable "rds_instance_type" {
+  description = "RDS instance type"
+  type = string
+  default = "db.m5.large"
+}
+
+variable "rds_allocated_storage" {
+  description = "RDS initial storage size (GiB)"
+  type = number
+  default = 60
+}
+
+variable "rds_max_allocated_storage" {
+  description = "RDS maximum storage size that will autoscale to (GiB)"
+  type = number
+  default = 300
+}
+
+variable "rds_backup_retention_period" {
+  description = "RDS Backup retention (days). Set to 0 to disable"
+  type = number
+  default = 7
+  validation {
+    condition = (var.rds_backup_retention_period <= 35)
+    error_message = "rds_backup_retention_period should be less than 36 days"
+  }
+}
+
+variable "rds_backup_before_deletion" {
+  description = "Create a snapshot before deleting the RDS instance"
+  type = bool
+  default = true
+}
+
+variable "rds_final_backup_name" {
+  description = "Snapshot name to use for the RDS final snapshot"
+  type = string
+  default = ""
+}
+
+variable "rds_postgresql_version" {
+  description = "RDS PostgreSQL version"
+  type = string
+  default = "15"
+}
index 7f433950fe99764d25f6490a198f79ef1747cf23..46bc2d170be6267713d499b37ef46c8d2a9f9863 100644 (file)
@@ -20,6 +20,7 @@ locals {
 
   private_subnet_id = one(aws_subnet.private_subnet[*]) != null ? one(aws_subnet.private_subnet[*]).id : var.private_subnet_id
   public_subnet_id = one(aws_subnet.public_subnet[*]) != null ? one(aws_subnet.public_subnet[*]).id : var.public_subnet_id
+  additional_rds_subnet_id = one(aws_subnet.additional_rds_subnet[*]) != null ? one(aws_subnet.additional_rds_subnet[*]).id : var.additional_rds_subnet_id
 
   public_hosts = var.private_only ? [] : var.user_facing_hosts
   private_hosts = concat(
@@ -38,4 +39,5 @@ locals {
       }
     ]
   ])
+  use_rds = var.use_rds
 }
index da98f1ac8357af95ba6bed2f8aa61027ed8a5783..dbd17e062cda922214528a46a6f9615ba2039c05 100644 (file)
@@ -62,6 +62,23 @@ resource "aws_subnet" "private_subnet" {
   }
 }
 
+#
+# Additional subnet on a different AZ is required if RDS is enabled
+#
+resource "aws_subnet" "additional_rds_subnet" {
+  count = (var.additional_rds_subnet_id == "" && local.use_rds) ? 1 : 0
+  vpc_id = local.arvados_vpc_id
+  availability_zone = data.aws_availability_zones.available.names[1]
+  cidr_block = "10.1.3.0/24"
+
+  lifecycle {
+    precondition {
+      condition = (var.vpc_id == "")
+      error_message = "additional_rds_subnet_id should be set if vpc_id is also set"
+    }
+  }
+}
+
 #
 # VPC S3 access
 #
index 9424193b52e99d61278f218b77ab06df81f29eae..dc2c8faebd65820716c7eba3108acd809f0a41d8 100644 (file)
@@ -21,6 +21,10 @@ output "arvados_sg_id" {
   value = local.arvados_sg_id
 }
 
+output "additional_rds_subnet_id" {
+  value = local.use_rds ? local.additional_rds_subnet_id : ""
+}
+
 output "eip_id" {
   value = { for k, v in aws_eip.arvados_eip: k => v.id }
 }
@@ -82,3 +86,7 @@ output "domain_name" {
 output "custom_tags" {
   value = var.custom_tags
 }
+
+output "use_rds" {
+  value = var.use_rds
+}
index 867034624429e49fb2646f18fccefaf072b95fd0..b2aab6233845e7dd850c6d46e129fbfd3b15888c 100644 (file)
 # Optional networking options. Set existing resources to be used instead of
 # creating new ones.
 # NOTE: We only support fully managed or fully custom networking, not a mix of both.
+#
 # vpc_id = "vpc-aaaa"
 # sg_id = "sg-bbbb"
 # public_subnet_id = "subnet-cccc"
 # private_subnet_id = "subnet-dddd"
+#
+# RDS related parameters:
+# use_rds = true
+# additional_rds_subnet_id = "subnet-eeee"
 
 # Optional custom tags to add to every resource. Default: {}
 # custom_tags = {
index c8d366a199dc435aa078fc13583a40c1df764e62..6721ffefa21d276deedd8f396eb5f7609863b1de 100644 (file)
@@ -79,6 +79,12 @@ variable "sg_id" {
   default = ""
 }
 
+variable "additional_rds_subnet_id" {
+  description = "Use existing subnet for RDS instead of creating one for the cluster"
+  type = string
+  default = ""
+}
+
 variable "private_subnet_id" {
   description = "Use existing private subnet instead of creating one for the cluster"
   type = string
@@ -95,4 +101,10 @@ variable "custom_tags" {
   description = "Apply customized tags to every resource on the cluster"
   type = map(string)
   default = {}
+}
+
+variable "use_rds" {
+  description = "Enable this to create an RDS instance as the cluster's database service"
+  type = bool
+  default = false
 }
\ No newline at end of file
index 9a493ec049deb878cbb99acc4a6403d583bcd3bf..9ea63133816f568ebd5fe04bb1e17b257d6c912b 100644 (file)
@@ -65,12 +65,12 @@ Installing on Red Hat, AlmaLinux, and Rocky Linux
 
 Arvados publishes packages for RHEL 8 and distributions based on it. Note that these packages depend on, and will automatically enable, the Python 3.9 module. You can install the Python SDK package on any of these distributions by running the following commands::
 
-  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<EOF
+  sudo tee /etc/yum.repos.d/arvados.repo >/dev/null <<'EOF'
   [arvados]
   name=Arvados
-  baseurl=http://rpm.arvados.org/CentOS/\$releasever/os/\$basearch/
+  baseurl=http://rpm.arvados.org/RHEL/$releasever/os/$basearch/
   gpgcheck=1
-  gpgkey=http://rpm.arvados.org/CentOS/RPM-GPG-KEY-arvados
+  gpgkey=http://rpm.arvados.org/RHEL/RPM-GPG-KEY-arvados
   EOF
   sudo dnf install python3-arvados-user-activity