changeset 0:95226b754d9e

initial release
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 17 Sep 2018 11:34:55 +0200
parents
children 734066ca3b7d
files AUTHORS CMakeLists.txt COPYING Framework/DicomInstanceInfo.cpp Framework/DicomInstanceInfo.h Framework/DownloadArea.cpp Framework/DownloadArea.h Framework/HttpQueries/DetectTransferPlugin.cpp Framework/HttpQueries/DetectTransferPlugin.h Framework/HttpQueries/HttpQueriesQueue.cpp Framework/HttpQueries/HttpQueriesQueue.h Framework/HttpQueries/HttpQueriesRunner.cpp Framework/HttpQueries/HttpQueriesRunner.h Framework/HttpQueries/IHttpQuery.h Framework/OrthancInstancesCache.cpp Framework/OrthancInstancesCache.h Framework/PullMode/BucketPullQuery.cpp Framework/PullMode/BucketPullQuery.h Framework/PullMode/PullJob.cpp Framework/PullMode/PullJob.h Framework/PushMode/ActivePushTransactions.cpp Framework/PushMode/ActivePushTransactions.h Framework/PushMode/BucketPushQuery.cpp Framework/PushMode/BucketPushQuery.h Framework/PushMode/PushJob.cpp Framework/PushMode/PushJob.h Framework/SourceDicomInstance.cpp Framework/SourceDicomInstance.h Framework/StatefulOrthancJob.cpp Framework/StatefulOrthancJob.h Framework/TransferBucket.cpp Framework/TransferBucket.h Framework/TransferQuery.cpp Framework/TransferQuery.h Framework/TransferScheduler.cpp Framework/TransferScheduler.h Framework/TransferToolbox.cpp Framework/TransferToolbox.h NEWS Plugin/Plugin.cpp Plugin/PluginContext.cpp Plugin/PluginContext.h README Resources/Orthanc/DownloadOrthancFramework.cmake Resources/Orthanc/LinuxStandardBaseToolchain.cmake Resources/Orthanc/MinGW-W64-Toolchain32.cmake Resources/Orthanc/MinGW-W64-Toolchain64.cmake Resources/Orthanc/MinGWToolchain.cmake Resources/OrthancExplorer.js Resources/SyncOrthancFolder.py UnitTests/UnitTestsMain.cpp
diffstat 51 files changed, 7297 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AUTHORS	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,11 @@
+Database plugins for Orthanc
+============================
+
+
+Authors
+-------
+
+* Osimis S.A. <info@osimis.io>
+  Rue du Bois Saint-Jean 15/1
+  4102 Seraing
+  Belgium
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CMakeLists.txt	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,162 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2018 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+cmake_minimum_required(VERSION 2.8)
+project(OrthancTransfersAccelerator)
+
+set(ORTHANC_PLUGIN_VERSION "mainline")
+
+if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.4.2")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+
+# Parameters of the build
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+set(ORTHANC_SDK_VERSION "framework" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"1.4.2\" or \"framework\")")
+
+# Advanced parameters to fine-tune linking against system libraries
+set(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK")
+
+
+# Download and setup the Orthanc framework
+include(${CMAKE_CURRENT_LIST_DIR}/Resources/Orthanc/DownloadOrthancFramework.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake)
+
+set(HAS_EMBEDDED_RESOURCES ON)
+set(ENABLE_LOCALE OFF)         # Disable support for locales (notably in Boost)
+set(ENABLE_GOOGLE_TEST ON)
+set(ENABLE_MODULE_IMAGES OFF)
+set(ENABLE_MODULE_JOBS OFF)
+set(ENABLE_MODULE_DICOM OFF)
+set(ENABLE_ZLIB ON)
+
+include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+include_directories(${ORTHANC_ROOT})
+
+
+# Check that the Orthanc SDK headers are available
+if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK)
+  if (ORTHANC_SDK_VERSION STREQUAL "1.4.2")
+    include_directories(${CMAKE_CURRENT_LIST_DIR}/Resources/Orthanc/Sdk-1.4.2)
+  elseif (ORTHANC_SDK_VERSION STREQUAL "framework")
+    include_directories(${ORTHANC_ROOT}/Plugins/Include)
+  else()
+    message(FATAL_ERROR "Unsupported version of the Orthanc plugin SDK: ${ORTHANC_SDK_VERSION}")
+  endif()
+else ()
+  CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCDatabasePlugin.h HAVE_ORTHANC_H)
+  if (NOT HAVE_ORTHANC_H)
+    message(FATAL_ERROR "Please install the headers of the Orthanc plugins SDK")
+  endif()
+endif()
+
+
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
+    ${ORTHANC_PLUGIN_VERSION} "TransfersAccelerator index plugin" OrthancTransfersAccelerator.dll
+    "TransfersAccelerator as a database back-end to Orthanc (index)"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/Version.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  set(PLUGIN_RESOURCES ${AUTOGENERATED_DIR}/Version.rc)
+endif()
+
+
+
+EmbedResources(
+  ORTHANC_EXPLORER  ${CMAKE_SOURCE_DIR}/Resources/OrthancExplorer.js
+  )
+
+set(FRAMEWORK_SOURCES
+  Framework/DicomInstanceInfo.cpp
+  Framework/DownloadArea.cpp
+  Framework/HttpQueries/DetectTransferPlugin.cpp
+  Framework/HttpQueries/HttpQueriesQueue.cpp
+  Framework/HttpQueries/HttpQueriesRunner.cpp
+  Framework/OrthancInstancesCache.cpp
+  Framework/PullMode/BucketPullQuery.cpp
+  Framework/PullMode/PullJob.cpp
+  Framework/PushMode/ActivePushTransactions.cpp
+  Framework/PushMode/BucketPushQuery.cpp
+  Framework/PushMode/PushJob.cpp
+  Framework/SourceDicomInstance.cpp
+  Framework/StatefulOrthancJob.cpp
+  Framework/TransferBucket.cpp
+  Framework/TransferQuery.cpp
+  Framework/TransferScheduler.cpp
+  Framework/TransferToolbox.cpp
+  ${ORTHANC_ROOT}/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+  )
+
+
+
+add_library(OrthancTransfersAccelerator SHARED
+  Plugin/Plugin.cpp
+  Plugin/PluginContext.cpp
+
+  ${FRAMEWORK_SOURCES}
+  ${PLUGIN_RESOURCES}
+  ${ORTHANC_CORE_SOURCES}
+  ${AUTOGENERATED_SOURCES}
+  )
+
+message("Setting the version of the library to ${ORTHANC_PLUGIN_VERSION}")
+
+add_definitions(
+  -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}"
+  -DHAS_ORTHANC_EXCEPTION=1
+  )
+
+set_target_properties(OrthancTransfersAccelerator PROPERTIES 
+  VERSION ${ORTHANC_PLUGIN_VERSION} 
+  SOVERSION ${ORTHANC_PLUGIN_VERSION}
+  COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=1
+  )
+
+install(
+  TARGETS OrthancTransfersAccelerator
+  RUNTIME DESTINATION lib    # Destination for Windows
+  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+  )
+
+add_executable(UnitTests
+  UnitTests/UnitTestsMain.cpp
+
+  ${FRAMEWORK_SOURCES}
+  ${ORTHANC_CORE_SOURCES}
+  ${GOOGLE_TEST_SOURCES}
+  )
+
+target_link_libraries(UnitTests ${GOOGLE_TEST_LIBRARIES})
+set_target_properties(UnitTests PROPERTIES
+  COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/COPYING	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/DicomInstanceInfo.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,88 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomInstanceInfo.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
+
+static const char *KEY_ID = "ID";
+static const char *KEY_MD5 = "MD5";
+static const char *KEY_SIZE = "Size";
+
+
+namespace OrthancPlugins
+{
+  DicomInstanceInfo::DicomInstanceInfo(const std::string& id,
+                                       const MemoryBuffer& buffer) :
+    id_(id),
+    size_(buffer.GetSize())
+  {
+    Orthanc::Toolbox::ComputeMD5(md5_, buffer.GetData(), buffer.GetSize());
+  }
+
+  
+  DicomInstanceInfo::DicomInstanceInfo(const std::string& id,
+                                       size_t size,
+                                       const std::string& md5) :
+    id_(id),
+    size_(size),
+    md5_(md5)
+  {
+  }
+
+
+  DicomInstanceInfo::DicomInstanceInfo(const Json::Value& serialized)
+  {
+    if (serialized.type() != Json::objectValue ||
+        !serialized.isMember(KEY_ID) ||
+        !serialized.isMember(KEY_SIZE) ||
+        !serialized.isMember(KEY_MD5) ||
+        serialized[KEY_ID].type() != Json::stringValue ||
+        serialized[KEY_SIZE].type() != Json::stringValue ||
+        serialized[KEY_MD5].type() != Json::stringValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      id_ = serialized[KEY_ID].asString();
+      md5_ = serialized[KEY_MD5].asString();
+        
+      try
+      {
+        size_ = boost::lexical_cast<size_t>(serialized[KEY_SIZE].asString());
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);          
+      }
+    }
+  }
+
+
+  void DicomInstanceInfo::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+    target[KEY_ID] = id_;
+    target[KEY_SIZE] = boost::lexical_cast<std::string>(size_);
+    target[KEY_MD5] = md5_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/DicomInstanceInfo.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,68 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <string>
+#include <json/value.h>
+
+namespace OrthancPlugins
+{
+  class MemoryBuffer;
+  
+  class DicomInstanceInfo
+  {
+  private:
+    std::string   id_;
+    size_t        size_;
+    std::string   md5_;
+
+  public:
+    DicomInstanceInfo() :
+      size_(0)
+    {
+    }
+
+    DicomInstanceInfo(const std::string& id,
+                      const MemoryBuffer& buffer);
+
+    DicomInstanceInfo(const std::string& id,
+                      size_t size,
+                      const std::string& md5);
+
+    DicomInstanceInfo(const Json::Value& serialized);
+
+    const std::string& GetId() const
+    {
+      return id_;
+    }
+
+    size_t GetSize() const
+    {
+      return size_;
+    }
+
+    const std::string& GetMD5() const
+    {
+      return md5_;
+    }
+
+    void Serialize(Json::Value& target) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/DownloadArea.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,325 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DownloadArea.h"
+
+#include <Core/Compression/GzipCompressor.h>
+#include <Core/Logging.h>
+#include <Core/SystemToolbox.h>
+#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
+
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+
+namespace OrthancPlugins
+{
+  class DownloadArea::Instance::Writer : public boost::noncopyable
+  {
+  private:
+    boost::filesystem::ofstream stream_;
+        
+  public:
+    Writer(Orthanc::TemporaryFile& f,
+           bool create) 
+    {
+      if (create)
+      {
+        // Create the file.
+        stream_.open(f.GetPath(), std::ofstream::out | std::ofstream::binary);
+      }
+      else
+      {
+        // Open the existing file to modify it. The "in" mode is
+        // necessary, otherwise previous content is lost by
+        // truncation (as an ofstream defaults to std::ios::trunc,
+        // the flag to truncate the existing content).
+        stream_.open(f.GetPath(), std::ofstream::in | std::ofstream::out | std::ofstream::binary);
+      }
+
+      if (!stream_.good())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_CannotWriteFile);
+      }
+    }
+
+    void Write(size_t offset,
+               const void* data,
+               size_t size)
+    {
+      stream_.seekp(offset);
+      stream_.write(reinterpret_cast<const char*>(data), size);
+    }
+  };
+
+
+  DownloadArea::Instance::Instance(const DicomInstanceInfo& info) :
+    info_(info)
+  {
+    Writer writer(file_, true);
+
+    // Create a sparse file of expected size
+    if (info_.GetSize() != 0)
+    {
+      writer.Write(info_.GetSize() - 1, "", 1);
+    }
+  }
+
+
+  void DownloadArea::Instance::WriteChunk(size_t offset,
+                                          const void* data,
+                                          size_t size)
+  {
+    if (offset + size > info_.GetSize())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else if (size > 0)
+    {
+      Writer writer(file_, false);
+      writer.Write(offset, data, size);
+    }
+  }
+
+  
+  void DownloadArea::Instance::Commit(OrthancPluginContext* context,
+                                      bool simulate) const
+  {
+    std::string content;
+    Orthanc::SystemToolbox::ReadFile(content, file_.GetPath());
+
+    std::string md5;
+    Orthanc::Toolbox::ComputeMD5(md5, content);
+
+    if (md5 == info_.GetMD5())
+    {
+      if (!simulate)
+      {
+        if (context == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+
+        Json::Value result;
+        if (!RestApiPost(result, context, "/instances", 
+                         content.empty() ? NULL : content.c_str(), content.size(),
+                         false))
+        {
+          LOG(ERROR) << "Cannot import a transfered DICOM instance into Orthanc: "
+                     << info_.GetId();
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile);
+        }
+      }
+    }
+    else
+    {
+      LOG(ERROR) << "Bad MD5 sum in a transfered DICOM instance: " << info_.GetId();
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile);
+    }
+  }
+
+
+  void DownloadArea::Clear()
+  {
+    for (Instances::iterator it = instances_.begin(); 
+         it != instances_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        delete it->second;
+        it->second = NULL;
+      }
+    }
+
+    instances_.clear();
+  }
+
+
+  DownloadArea::Instance& DownloadArea::LookupInstance(const std::string& id)
+  {
+    Instances::iterator it = instances_.find(id);
+
+    if (it == instances_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+    else if (it->first != id ||
+             it->second == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      return *it->second;
+    }
+  }
+
+
+  void DownloadArea::WriteUncompressedBucket(const TransferBucket& bucket,
+                                             const void* data,
+                                             size_t size)
+  {
+    if (size != bucket.GetTotalSize())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }
+
+    if (size == 0)
+    {
+      return;
+    }
+
+    size_t pos = 0;
+
+    for (size_t i = 0; i < bucket.GetChunksCount(); i++)
+    {
+      size_t chunkSize = bucket.GetChunkSize(i);
+      size_t offset = bucket.GetChunkOffset(i);
+
+      if (pos + chunkSize > size)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      Instance& instance = LookupInstance(bucket.GetChunkInstanceId(i));
+      instance.WriteChunk(offset, reinterpret_cast<const char*>(data) + pos, chunkSize);
+
+      pos += chunkSize;
+    }
+
+    if (pos != size)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  void DownloadArea::Setup(const std::vector<DicomInstanceInfo>& instances)
+  {
+    totalSize_ = 0;
+      
+    for (size_t i = 0; i < instances.size(); i++)
+    {
+      const std::string& id = instances[i].GetId();
+        
+      assert(instances_.find(id) == instances_.end());
+      instances_[id] = new Instance(instances[i]);
+
+      totalSize_ += instances[i].GetSize();
+    }
+  }
+    
+
+  void DownloadArea::CommitInternal(OrthancPluginContext* context,
+                                    bool simulate)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+      
+    for (Instances::iterator it = instances_.begin(); 
+         it != instances_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        it->second->Commit(context, simulate);
+        delete it->second;
+        it->second = NULL;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  DownloadArea::DownloadArea(const TransferScheduler& scheduler)
+  {
+    std::vector<DicomInstanceInfo> instances;
+    scheduler.ListInstances(instances);
+    Setup(instances);
+  }
+
+
+  void DownloadArea::WriteBucket(const TransferBucket& bucket,
+                                 const void* data,
+                                 size_t size,
+                                 BucketCompression compression)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+      
+    switch (compression)
+    {
+      case BucketCompression_None:
+        WriteUncompressedBucket(bucket, data, size);
+        break;
+          
+      case BucketCompression_Gzip:
+      {
+        std::string uncompressed;
+        Orthanc::GzipCompressor compressor;
+        compressor.Uncompress(uncompressed, data, size);
+        WriteUncompressedBucket(bucket, uncompressed.c_str(), uncompressed.size());
+        break;
+      }
+
+      default:          
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void DownloadArea::WriteInstance(const std::string& instanceId,
+                                   const void* data,
+                                   size_t size)
+  {
+    std::string md5;
+    Orthanc::Toolbox::ComputeMD5(md5, data, size);
+      
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      Instances::const_iterator it = instances_.find(instanceId);
+      if (it == instances_.end() ||
+          it->second == NULL ||
+          it->second->GetInfo().GetId() != instanceId ||
+          it->second->GetInfo().GetSize() != size ||
+          it->second->GetInfo().GetMD5() != md5)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile);
+      }
+      else
+      {
+        it->second->WriteChunk(0, data, size);
+      }
+    }
+  }
+
+
+  void DownloadArea::CheckMD5()
+  {
+    LOG(INFO) << "Checking MD5 sum without committing (testing)";
+    CommitInternal(NULL, true);
+  }
+
+
+  void DownloadArea::Commit(OrthancPluginContext* context)
+  {
+    LOG(INFO) << "Importing transfered DICOM files from the temporary download area into Orthanc";
+    CommitInternal(context, false);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/DownloadArea.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,107 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "TransferScheduler.h"
+
+#include <Core/TemporaryFile.h>
+
+namespace OrthancPlugins
+{
+  class DownloadArea : public boost::noncopyable
+  {
+  private:
+    class Instance : public boost::noncopyable
+    {
+    private:
+      DicomInstanceInfo       info_;
+      Orthanc::TemporaryFile  file_;
+
+      class Writer;
+
+    public:
+      Instance(const DicomInstanceInfo& info);
+
+      const DicomInstanceInfo& GetInfo() const
+      {
+        return info_;
+      }
+
+      void WriteChunk(size_t offset,
+                      const void* data,
+                      size_t size);
+
+      void Commit(OrthancPluginContext* context,
+                  bool simulate) const;
+    };
+
+
+    typedef std::map<std::string, Instance*>   Instances;
+
+    boost::mutex  mutex_;
+    Instances     instances_;
+    size_t        totalSize_;
+
+
+    void Clear();
+
+    Instance& LookupInstance(const std::string& id);
+
+    void WriteUncompressedBucket(const TransferBucket& bucket,
+                                 const void* data,
+                                 size_t size);
+
+    void Setup(const std::vector<DicomInstanceInfo>& instances);
+    
+    void CommitInternal(OrthancPluginContext* context,
+                        bool simulate);
+
+  public:
+    DownloadArea(const TransferScheduler& scheduler);
+
+    DownloadArea(const std::vector<DicomInstanceInfo>& instances)
+    {
+      Setup(instances);
+    }
+
+    ~DownloadArea()
+    {
+      Clear();
+    }
+
+    size_t GetTotalSize() const
+    {
+      return totalSize_;
+    }
+
+    void WriteBucket(const TransferBucket& bucket,
+                     const void* data,
+                     size_t size,
+                     BucketCompression compression);
+
+    void WriteInstance(const std::string& instanceId,
+                       const void* data,
+                       size_t size);
+
+    void CheckMD5();
+
+    void Commit(OrthancPluginContext* context);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/HttpQueries/DetectTransferPlugin.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,90 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DetectTransferPlugin.h"
+
+#include "../TransferToolbox.h"
+#include "HttpQueriesRunner.h"
+
+#include <Core/OrthancException.h>
+
+#include <json/reader.h>
+
+
+namespace OrthancPlugins
+{
+  DetectTransferPlugin::DetectTransferPlugin(std::set<std::string>&  target,
+                                             const std::string& peer) :
+    target_(target),
+    peer_(peer),
+    uri_(URI_PLUGINS)
+  {
+  }
+
+
+  void DetectTransferPlugin::ReadBody(std::string& body) const
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+
+  void DetectTransferPlugin::HandleAnswer(const void* answer,
+                                          size_t size)
+  {
+    Json::Reader reader;
+    Json::Value value;
+
+    if (reader.parse(reinterpret_cast<const char*>(answer), 
+                     reinterpret_cast<const char*>(answer) + size, value) &&
+        value.type() == Json::arrayValue)
+    {
+      for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+      {
+        if (value[i].type() == Json::stringValue &&
+            value[i].asString() == PLUGIN_NAME)
+        {
+          target_.insert(peer_);
+        }
+      }
+    }
+  }
+
+
+  void DetectTransferPlugin::Apply(std::set<std::string>& activePeers,
+                                   OrthancPluginContext* context,
+                                   size_t threadsCount,
+                                   unsigned int timeout)
+  {
+    OrthancPlugins::HttpQueriesQueue queue(context);
+
+    queue.GetOrthancPeers().SetTimeout(timeout);
+    queue.Reserve(queue.GetOrthancPeers().GetPeersCount());
+
+    for (size_t i = 0; i < queue.GetOrthancPeers().GetPeersCount(); i++)
+    {
+      queue.Enqueue(new OrthancPlugins::DetectTransferPlugin
+                    (activePeers, queue.GetOrthancPeers().GetPeerName(i)));
+    }
+
+    {
+      OrthancPlugins::HttpQueriesRunner runner(queue, threadsCount);
+      queue.WaitComplete();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/HttpQueries/DetectTransferPlugin.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,66 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IHttpQuery.h"
+
+#include <orthanc/OrthancCPlugin.h>
+#include <set>
+
+
+namespace OrthancPlugins
+{
+  class DetectTransferPlugin : public IHttpQuery
+  {
+  private:
+    std::set<std::string>&  target_;
+    std::string             peer_;
+    std::string             uri_;
+
+  public:
+    DetectTransferPlugin(std::set<std::string>&  target,
+                         const std::string& peer);
+
+    virtual Orthanc::HttpMethod GetMethod() const
+    {
+      return Orthanc::HttpMethod_Get;
+    }
+
+    virtual const std::string& GetPeer() const
+    {
+      return peer_;
+    }
+
+    virtual const std::string& GetUri() const
+    {
+      return uri_;
+    }
+
+    virtual void ReadBody(std::string& body) const;
+
+    virtual void HandleAnswer(const void* answer,
+                              size_t size);
+
+    static void Apply(std::set<std::string>& activePeers,
+                      OrthancPluginContext* context,
+                      size_t threadsCount,
+                      unsigned int timeout);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/HttpQueries/HttpQueriesQueue.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,281 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "HttpQueriesQueue.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancPlugins
+{
+  HttpQueriesQueue::Status HttpQueriesQueue::GetStatusInternal() const
+  {
+    if (successQueries_ == queries_.size())
+    {
+      return Status_Success;
+    }
+    else if (isFailure_)
+    {
+      return Status_Failure;
+    }
+    else
+    {
+      return Status_Running;
+    }
+  }
+
+
+  HttpQueriesQueue::HttpQueriesQueue(OrthancPluginContext* context) :
+    context_(context),
+    peers_(context),
+    maxRetries_(0)
+  {
+    Reset();
+  }
+
+    
+  HttpQueriesQueue::~HttpQueriesQueue()
+  {
+    for (size_t i = 0; i < queries_.size(); i++)
+    {
+      assert(queries_[i] != NULL);
+      delete queries_[i];
+    }
+  }
+
+    
+  unsigned int HttpQueriesQueue::GetMaxRetries()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return maxRetries_;
+  }
+
+    
+  void HttpQueriesQueue::SetMaxRetries(unsigned int maxRetries)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    maxRetries_ = maxRetries;
+  }
+
+    
+  void HttpQueriesQueue::Reserve(size_t size)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    queries_.reserve(size);
+  }
+
+    
+  void HttpQueriesQueue::Reset()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    position_ = 0;
+    downloadedSize_ = 0;
+    uploadedSize_ = 0;
+    successQueries_ = 0;
+    isFailure_ = false;
+  }
+    
+
+  void HttpQueriesQueue::Enqueue(IHttpQuery* query)
+  {
+    if (query == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      queries_.push_back(query);
+    }
+  }
+    
+
+  bool HttpQueriesQueue::ExecuteOneQuery(uint64_t& networkTraffic)
+  {
+    networkTraffic = 0;
+      
+    unsigned int maxRetries;
+    IHttpQuery* query = NULL;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      maxRetries = maxRetries_;
+        
+      if (position_ == queries_.size() ||
+          isFailure_)
+      {
+        return false;
+      }
+      else
+      {
+        query = queries_[position_];
+        position_ ++;
+      }
+    }
+
+    std::string body;
+
+    if (query->GetMethod() == Orthanc::HttpMethod_Post ||
+        query->GetMethod() == Orthanc::HttpMethod_Put)
+    {
+      query->ReadBody(body);              
+    }       
+
+    unsigned int retry = 0;
+
+    for (;;)
+    {
+      MemoryBuffer answer(context_);
+
+      bool success;
+
+      try
+      {
+        switch (query->GetMethod())
+        {
+          case Orthanc::HttpMethod_Get:
+            success = peers_.DoGet(answer, query->GetPeer(), query->GetUri());
+            break;
+
+          case Orthanc::HttpMethod_Post:
+            success = peers_.DoPost(answer, query->GetPeer(), query->GetUri(), body);
+            break;
+
+          case Orthanc::HttpMethod_Put:
+            success = peers_.DoPut(query->GetPeer(), query->GetUri(), body);
+            break;
+
+          case Orthanc::HttpMethod_Delete:
+            success = peers_.DoDelete(query->GetPeer(), query->GetUri());
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        LOG(ERROR) << "Unhandled exception during an HTTP query to peer \"" 
+                   << query->GetPeer() << "\": " << e.What();
+        success = false;
+      }
+
+      if (success)
+      {
+        size_t downloaded = 0;
+        size_t uploaded = 0;
+
+        if (query->GetMethod() == Orthanc::HttpMethod_Get ||
+            query->GetMethod() == Orthanc::HttpMethod_Post)
+        {
+          query->HandleAnswer(answer.GetData(), answer.GetSize());
+          downloaded = answer.GetSize();
+        }
+
+        if (query->GetMethod() == Orthanc::HttpMethod_Put ||
+            query->GetMethod() == Orthanc::HttpMethod_Post)
+        {
+          uploaded = body.size();
+        }
+          
+        networkTraffic = downloaded + uploaded;
+            
+        {
+          boost::mutex::scoped_lock lock(mutex_);
+          downloadedSize_ += downloaded;
+          uploadedSize_ += uploaded;
+          successQueries_ ++;
+
+          if (successQueries_ == queries_.size())
+          {
+            completed_.notify_all();
+          }
+
+          return true;
+        }
+      }
+      else
+      {
+        // Error: Let's retry
+        retry ++;
+
+        if (retry < maxRetries)
+        {
+          // Wait 1 second before retrying
+          boost::this_thread::sleep(boost::posix_time::seconds(1));
+        }
+        else
+        {
+          LOG(ERROR) << "Reached the maximum number of retries for a HTTP query";
+
+          {
+            boost::mutex::scoped_lock lock(mutex_);
+            isFailure_ = true;
+            completed_.notify_all();
+          }
+
+          return false;
+        }
+      }
+    }
+  }
+
+
+  HttpQueriesQueue::Status HttpQueriesQueue::WaitComplete(unsigned int timeoutMS)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Status status = GetStatusInternal();
+
+    if (status == Status_Running)
+    {
+      completed_.timed_wait(lock, boost::posix_time::milliseconds(timeoutMS));
+      return GetStatusInternal();
+    }
+    else
+    {
+      return status;
+    }
+  }
+
+
+  void HttpQueriesQueue::WaitComplete()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    while (GetStatusInternal() == Status_Running)
+    {
+      completed_.timed_wait(lock, boost::posix_time::milliseconds(200));
+    }
+  }
+
+
+  void HttpQueriesQueue::GetStatistics(size_t& scheduledQueriesCount,
+                                       size_t& successQueriesCount,
+                                       uint64_t& downloadedSize,
+                                       uint64_t& uploadedSize)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    scheduledQueriesCount = queries_.size();
+    successQueriesCount = successQueries_;
+    downloadedSize = downloadedSize_;
+    uploadedSize = uploadedSize_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/HttpQueries/HttpQueriesQueue.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,90 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IHttpQuery.h"
+
+#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
+
+#include <boost/thread/mutex.hpp>
+#include <boost/thread/condition_variable.hpp>
+
+
+namespace OrthancPlugins
+{
+  class HttpQueriesQueue : public boost::noncopyable
+  {
+  public:
+    enum Status
+    {
+      Status_Running,
+      Status_Success,
+      Status_Failure
+    };
+
+  private:
+    OrthancPluginContext         *context_;
+    OrthancPeers                  peers_;
+    boost::mutex                  mutex_;
+    boost::condition_variable     completed_;
+    std::vector<IHttpQuery*>      queries_;
+    unsigned int                  maxRetries_;
+
+    size_t                        position_;
+    uint64_t                      downloadedSize_;   // GET answers + POST answers
+    uint64_t                      uploadedSize_;     // PUT body + POST body
+    size_t                        successQueries_;
+    bool                          isFailure_;
+
+
+    Status GetStatusInternal() const;
+
+  public:
+    HttpQueriesQueue(OrthancPluginContext* context);
+
+    ~HttpQueriesQueue();
+
+    OrthancPeers& GetOrthancPeers()
+    {
+      return peers_;
+    }
+
+    unsigned int GetMaxRetries();
+
+    void SetMaxRetries(unsigned int maxRetries);
+
+    void Reserve(size_t size);
+
+    void Reset();
+
+    void Enqueue(IHttpQuery* query);  // Takes ownership
+
+    bool ExecuteOneQuery(uint64_t& networkTraffic);
+
+    Status WaitComplete(unsigned int timeoutMS);
+    
+    void WaitComplete();
+
+    void GetStatistics(size_t& scheduledQueriesCount,
+                       size_t& successQueriesCount,
+                       uint64_t& downloadedSize,
+                       uint64_t& uploadedSize);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/HttpQueries/HttpQueriesRunner.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,108 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "HttpQueriesRunner.h"
+
+#include <Core/OrthancException.h>
+
+#include <boost/thread.hpp>
+
+
+namespace OrthancPlugins
+{
+  void HttpQueriesRunner::Worker(HttpQueriesRunner* that)
+  {
+    while (that->continue_)
+    {
+      size_t size;
+        
+      if (that->queue_.ExecuteOneQuery(size))
+      {
+        boost::mutex::scoped_lock lock(that->mutex_);
+        that->totalTraffic_ += size;
+        that->lastUpdate_ = boost::posix_time::microsec_clock::local_time();
+      }
+      else
+      {
+        // We're done (either failure, or no more pending queries)
+        return;
+      }
+    }
+  }
+
+
+  HttpQueriesRunner::HttpQueriesRunner(HttpQueriesQueue& queue,
+                                       size_t threadsCount) :
+    queue_(queue),
+    continue_(true),
+    start_(boost::posix_time::microsec_clock::local_time()),
+    totalTraffic_(0),
+    lastUpdate_(start_)
+  {
+    if (threadsCount == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+      
+    workers_.resize(threadsCount);
+
+    for (size_t i = 0; i < threadsCount; i++)
+    {
+      workers_[i] = new boost::thread(Worker, this);
+    }
+  }
+
+
+  HttpQueriesRunner::~HttpQueriesRunner()
+  {
+    continue_ = false;
+
+    for (size_t i = 0; i < workers_.size(); i++)
+    {
+      if (workers_[i] != NULL)
+      {
+        if (workers_[i]->joinable())
+        {
+          workers_[i]->join();
+        }
+
+        delete workers_[i];
+      }
+    }
+  }
+
+    
+  void HttpQueriesRunner::GetSpeed(float& kilobytesPerSecond)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+      
+    double ms = static_cast<double>((lastUpdate_ - start_).total_milliseconds());
+
+    if (ms < 10.0)
+    {
+      // Prevents division by zero on very quick transfers
+      kilobytesPerSecond = 0;
+    }
+    else
+    {
+      kilobytesPerSecond = static_cast<float>(
+        static_cast<double>(totalTraffic_) * 1000.0 /*ms*/ / (1024.0 /*KB*/ * ms));
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/HttpQueries/HttpQueriesRunner.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,48 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "HttpQueriesQueue.h"
+
+namespace OrthancPlugins
+{
+  class HttpQueriesRunner : public boost::noncopyable
+  {
+  private:
+    HttpQueriesQueue&            queue_;
+    std::vector<boost::thread*>  workers_;
+    bool                         continue_;
+    boost::posix_time::ptime     start_;
+
+    boost::mutex                 mutex_;
+    size_t                       totalTraffic_;
+    boost::posix_time::ptime     lastUpdate_;
+
+    static void Worker(HttpQueriesRunner* that);
+
+  public:
+    HttpQueriesRunner(HttpQueriesQueue& queue,
+                      size_t threadsCount);
+
+    ~HttpQueriesRunner();
+
+    void GetSpeed(float& kilobytesPerSecond);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/HttpQueries/IHttpQuery.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,47 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Core/Enumerations.h>
+
+#include <boost/noncopyable.hpp>
+
+
+namespace OrthancPlugins
+{
+  class IHttpQuery : public boost::noncopyable
+  {
+  public:
+    virtual ~IHttpQuery()
+    {
+    }
+
+    virtual Orthanc::HttpMethod GetMethod() const = 0;
+
+    virtual const std::string& GetPeer() const = 0;
+
+    virtual const std::string& GetUri() const = 0;
+
+    virtual void ReadBody(std::string& body) const = 0;   // Only for PUT/POST
+
+    virtual void HandleAnswer(const void* answer,
+                              size_t size) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OrthancInstancesCache.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,279 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancInstancesCache.h"
+
+
+namespace OrthancPlugins
+{
+  void OrthancInstancesCache::CacheAccessor::CheckValid() const
+  {
+    if (instance_ == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+      
+
+  OrthancInstancesCache::CacheAccessor::CacheAccessor(OrthancInstancesCache& cache,
+                                                      const std::string& instanceId) :
+    lock_(cache.mutex_),
+    instance_(NULL)
+  {
+    cache.CheckInvariants();
+      
+    if (cache.index_.Contains(instanceId))
+    {
+      // Move the instance at the end of the LRU recycling
+      cache.index_.MakeMostRecent(instanceId);
+        
+      Content::const_iterator instance = cache.content_.find(instanceId);
+      assert(instance != cache.content_.end() &&
+             instance->first == instanceId &&
+             instance->second != NULL);
+
+      instance_ = instance->second;
+    }
+  }
+
+
+  const DicomInstanceInfo& OrthancInstancesCache::CacheAccessor::GetInfo() const
+  {
+    CheckValid();
+    return instance_->GetInfo();
+  }
+
+
+  void OrthancInstancesCache::CacheAccessor::GetChunk(std::string& chunk,
+                                                      std::string& md5,
+                                                      size_t offset,
+                                                      size_t size)
+  {
+    CheckValid();
+    return instance_->GetChunk(chunk, md5, offset, size);
+  }
+
+
+  void OrthancInstancesCache::CheckInvariants()
+  {
+#ifndef NDEBUG  
+    size_t s = 0;
+
+    assert(content_.size() == index_.GetSize());
+      
+    for (Content::const_iterator it = content_.begin();
+         it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      s += it->second->GetInfo().GetSize();
+
+      assert(index_.Contains(it->first));
+    }
+
+    assert(s == memorySize_);
+      
+    if (memorySize_ > maxMemorySize_)
+    {
+      // It is only allowed to overtake the max memory size if the
+      // cache contains a single, large DICOM instance
+      assert(index_.GetSize() == 1 &&
+             content_.size() == 1 &&
+             memorySize_ == (content_.begin())->second->GetInfo().GetSize());
+    }
+#endif
+  }
+
+
+  void OrthancInstancesCache::RemoveOldest()
+  {
+    CheckInvariants();
+
+    assert(!index_.IsEmpty());
+
+    std::string oldest = index_.RemoveOldest();
+
+    Content::iterator instance = content_.find(oldest);
+    assert(instance != content_.end() &&
+           instance->second != NULL);
+
+    memorySize_ -= instance->second->GetInfo().GetSize();
+    delete instance->second;
+    content_.erase(instance);
+  }
+
+
+  void OrthancInstancesCache::Store(const std::string& instanceId,
+                                    std::auto_ptr<SourceDicomInstance>& instance)
+  {
+    if (instance.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+      
+    if (index_.Contains(instanceId))
+    {
+      // This instance has been read by another thread since the cache
+      // lookup, give up
+      index_.MakeMostRecent(instanceId);
+      return;
+    }
+    else
+    {
+      // Make room in the cache for the new instance
+      while (!index_.IsEmpty() &&
+             memorySize_ + instance->GetInfo().GetSize() > maxMemorySize_)
+      {
+        RemoveOldest();
+      }
+
+      CheckInvariants();
+
+      index_.AddOrMakeMostRecent(instanceId);
+      memorySize_ += instance->GetInfo().GetSize();
+      content_[instanceId] = instance.release();
+
+      CheckInvariants();
+    }
+  }
+    
+
+  OrthancInstancesCache::OrthancInstancesCache(OrthancPluginContext* context) :
+    context_(context),
+    memorySize_(0),
+    maxMemorySize_(512 * MB)  // 512 MB by default
+  {
+    if (context == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+    
+
+  OrthancInstancesCache::~OrthancInstancesCache()
+  {
+    CheckInvariants();
+      
+    for (Content::iterator it = content_.begin();
+         it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+  
+
+  size_t OrthancInstancesCache::GetMemorySize() 
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return memorySize_;
+  }
+    
+
+  size_t OrthancInstancesCache::GetMaxMemorySize()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return maxMemorySize_;
+  }
+    
+
+  void OrthancInstancesCache::SetMaxMemorySize(size_t size)
+  {
+    if (size <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    
+    boost::mutex::scoped_lock lock(mutex_);
+
+    while (memorySize_ > size)
+    {
+      RemoveOldest();
+    }
+
+    maxMemorySize_ = size;
+    CheckInvariants();      
+  }
+
+
+  void OrthancInstancesCache::GetInstanceInfo(size_t& size,
+                                              std::string& md5,
+                                              const std::string& instanceId)
+  {
+    // Check whether the instance is part of the cache
+    {
+      CacheAccessor accessor(*this, instanceId);
+      if (accessor.IsValid())
+      {
+        size = accessor.GetInfo().GetSize();
+        md5 = accessor.GetInfo().GetMD5();
+        return;
+      }
+    }
+      
+    // The instance was not in the cache, load it
+    std::auto_ptr<SourceDicomInstance> instance(new SourceDicomInstance(context_, instanceId));
+    size = instance->GetInfo().GetSize();
+    md5 = instance->GetInfo().GetMD5();
+
+    // Store the just-loaded DICOM instance into the cache
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      Store(instanceId, instance);
+    }
+  }
+      
+    
+  void OrthancInstancesCache::GetChunk(std::string& chunk,
+                                       std::string& md5,
+                                       const std::string& instanceId,
+                                       size_t offset,
+                                       size_t size)
+  {
+    // Check whether the instance is part of the cache
+    {
+      CacheAccessor accessor(*this, instanceId);
+      if (accessor.IsValid())
+      {
+        accessor.GetChunk(chunk, md5, offset, size);
+        return;
+      }
+    }
+      
+    // The instance was not in the cache, load it
+    std::auto_ptr<SourceDicomInstance> instance(new SourceDicomInstance(context_, instanceId));
+    instance->GetChunk(chunk, md5, 0, instance->GetInfo().GetSize());
+
+    // Store the just-loaded DICOM instance into the cache
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      Store(instanceId, instance);
+    }
+  }    
+
+
+  void OrthancInstancesCache::GetChunk(std::string& chunk,
+                                       std::string& md5,
+                                       const TransferBucket& bucket,
+                                       size_t chunkIndex)
+  {
+    GetChunk(chunk, md5, bucket.GetChunkInstanceId(chunkIndex),
+             bucket.GetChunkOffset(chunkIndex),
+             bucket.GetChunkSize(chunkIndex));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OrthancInstancesCache.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,113 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "SourceDicomInstance.h"
+#include "TransferBucket.h"
+
+#include <Core/Cache/LeastRecentlyUsedIndex.h>
+
+#include <boost/thread/mutex.hpp>
+
+namespace OrthancPlugins
+{
+  class OrthancInstancesCache : public boost::noncopyable
+  {
+  private:
+    class CacheAccessor : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock  lock_;
+      SourceDicomInstance       *instance_;
+
+      void CheckValid() const;
+      
+    public:
+      CacheAccessor(OrthancInstancesCache& cache,
+                    const std::string& instanceId);
+
+      bool IsValid() const
+      {
+        return instance_ != NULL;
+      }
+
+      const DicomInstanceInfo& GetInfo() const;
+
+      void GetChunk(std::string& chunk,
+                    std::string& md5,
+                    size_t offset,
+                    size_t size);
+    };
+
+    
+    typedef Orthanc::LeastRecentlyUsedIndex<std::string>  Index;
+    typedef std::map<std::string, SourceDicomInstance*>   Content;
+
+    OrthancPluginContext*  context_;
+    boost::mutex           mutex_;
+    Index                  index_;
+    Content                content_;
+    size_t                 memorySize_;
+    size_t                 maxMemorySize_;
+
+
+    // The mutex must be locked!
+    void CheckInvariants();
+    
+    // The mutex must be locked!
+    void RemoveOldest();
+
+    // The mutex must be locked!
+    void Store(const std::string& instanceId,
+               std::auto_ptr<SourceDicomInstance>& instance);
+    
+
+  public:
+    OrthancInstancesCache(OrthancPluginContext* context);
+
+    ~OrthancInstancesCache();
+
+    OrthancPluginContext* GetContext() const
+    {
+      return context_;
+    }    
+
+    size_t GetMemorySize();
+
+    size_t GetMaxMemorySize();
+
+    void SetMaxMemorySize(size_t size);
+    
+    void GetInstanceInfo(size_t& size,
+                         std::string& md5,
+                         const std::string& instanceId);
+    
+    void GetChunk(std::string& chunk,
+                  std::string& md5,
+                  const std::string& instanceId,
+                  size_t offset,
+                  size_t size);
+
+    void GetChunk(std::string& chunk,
+                  std::string& md5,
+                  const TransferBucket& bucket,
+                  size_t chunkIndex);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PullMode/BucketPullQuery.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,49 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "BucketPullQuery.h"
+
+
+namespace OrthancPlugins
+{
+  BucketPullQuery::BucketPullQuery(DownloadArea& area,
+                                   const TransferBucket& bucket,
+                                   const std::string& peer,
+                                   BucketCompression compression) :
+    area_(area),
+    bucket_(bucket),
+    peer_(peer),
+    compression_(compression)
+  {
+    bucket_.ComputePullUri(uri_, compression_);
+  }
+
+
+  void BucketPullQuery::ReadBody(std::string& body) const
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+  
+  void BucketPullQuery::HandleAnswer(const void* answer,
+                                     size_t size)
+  {
+    area_.WriteBucket(bucket_, answer, size, compression_);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PullMode/BucketPullQuery.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,63 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../HttpQueries/IHttpQuery.h"
+#include "../DownloadArea.h"
+
+
+namespace OrthancPlugins
+{
+  class BucketPullQuery : public IHttpQuery
+  {
+  private:
+    DownloadArea&      area_;
+    TransferBucket     bucket_;
+    std::string        peer_;
+    std::string        uri_;
+    BucketCompression  compression_;
+
+  public:
+    BucketPullQuery(DownloadArea& area,
+                    const TransferBucket& bucket,
+                    const std::string& peer,
+                    BucketCompression compression);
+
+    virtual Orthanc::HttpMethod GetMethod() const
+    {
+      return Orthanc::HttpMethod_Get;
+    }
+
+    virtual const std::string& GetPeer() const
+    {
+      return peer_;
+    }
+
+    virtual const std::string& GetUri() const
+    {
+      return uri_;
+    }
+
+    virtual void ReadBody(std::string& body) const;
+
+    virtual void HandleAnswer(const void* answer,
+                              size_t size);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PullMode/PullJob.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,254 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PullJob.h"
+
+#include "BucketPullQuery.h"
+#include "../HttpQueries/HttpQueriesRunner.h"
+#include "../TransferScheduler.h"
+
+#include <Core/Logging.h>
+
+#include <json/writer.h>
+
+
+namespace OrthancPlugins
+{
+  class PullJob::CommitState : public IState
+  {
+  private:
+    const PullJob&               job_;
+    std::auto_ptr<DownloadArea>  area_;
+
+  public:
+    CommitState(const PullJob& job,
+                DownloadArea* area /* takes ownership */) :
+      job_(job),
+      area_(area)
+    {
+    }
+
+    virtual StateUpdate* Step()
+    {
+      area_->Commit(job_.context_);
+      return StateUpdate::Success();
+    }
+
+    virtual void Stop(OrthancPluginJobStopReason reason)
+    {
+    }
+  };
+
+
+  class PullJob::PullBucketsState : public IState
+  {
+  private:
+    const PullJob&                    job_;
+    JobInfo&                          info_;
+    HttpQueriesQueue                  queue_;
+    std::auto_ptr<DownloadArea>       area_;
+    std::auto_ptr<HttpQueriesRunner>  runner_;
+
+    void UpdateInfo()
+    {
+      size_t scheduledQueriesCount, completedQueriesCount;
+      uint64_t uploadedSize, downloadedSize;
+      queue_.GetStatistics(scheduledQueriesCount, completedQueriesCount, downloadedSize, uploadedSize);
+
+      info_.SetContent("DownloadedSizeMB", ConvertToMegabytes(downloadedSize));
+      info_.SetContent("CompletedHttpQueries", static_cast<unsigned int>(completedQueriesCount));
+
+      if (runner_.get() != NULL)
+      {
+        float speed;
+        runner_->GetSpeed(speed);
+        info_.SetContent("NetworkSpeedKBs", static_cast<unsigned int>(speed));
+      }
+            
+      // The "2" below corresponds to the "LookupInstancesState"
+      // and "CommitState" steps (which prevents division by zero)
+      info_.SetProgress(static_cast<float>(1 /* LookupInstancesState */ + completedQueriesCount) / 
+                        static_cast<float>(2 + scheduledQueriesCount));
+    }
+
+  public:
+    PullBucketsState(const PullJob&  job,
+                     JobInfo& info,
+                     const TransferScheduler& scheduler) :
+      job_(job),
+      info_(info),
+      queue_(job.context_),
+      area_(new DownloadArea(scheduler))
+    {
+      const std::string baseUrl = job.peers_.GetPeerUrl(job.query_.GetPeer());
+
+      std::vector<TransferBucket> buckets;
+      scheduler.ComputePullBuckets(buckets, job.targetBucketSize_, 2 * job.targetBucketSize_,
+                                   baseUrl, job.query_.GetCompression());
+      area_.reset(new DownloadArea(scheduler));
+        
+      queue_.Reserve(buckets.size());
+        
+      for (size_t i = 0; i < buckets.size(); i++)
+      {
+        queue_.Enqueue(new BucketPullQuery(*area_, buckets[i], job.query_.GetPeer(), job.query_.GetCompression()));
+      }
+
+      info_.SetContent("TotalInstances", static_cast<unsigned int>(scheduler.GetInstancesCount()));
+      info_.SetContent("TotalSizeMB", ConvertToMegabytes(scheduler.GetTotalSize()));
+      UpdateInfo();
+    }
+      
+    virtual StateUpdate* Step()
+    {
+      if (runner_.get() == NULL)
+      {
+        runner_.reset(new HttpQueriesRunner(queue_, job_.threadsCount_));
+      }
+
+      HttpQueriesQueue::Status status = queue_.WaitComplete(200);
+
+      UpdateInfo();
+
+      switch (status)
+      {
+        case HttpQueriesQueue::Status_Running:
+          return StateUpdate::Continue();
+
+        case HttpQueriesQueue::Status_Success:
+          return StateUpdate::Next(new CommitState(job_, area_.release()));
+
+        case HttpQueriesQueue::Status_Failure:
+          return StateUpdate::Failure();
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }        
+    }
+
+    virtual void Stop(OrthancPluginJobStopReason reason)
+    {
+      // Cancel the running download threads
+      runner_.reset();
+    }
+  };
+    
+
+  class PullJob::LookupInstancesState : public IState
+  {
+  private:
+    const PullJob&  job_;
+    JobInfo&        info_;
+
+  public:
+    LookupInstancesState(const PullJob& job,
+                         JobInfo& info) :
+      job_(job),
+      info_(info)
+    {
+      info_.SetContent("Peer", job_.query_.GetPeer());
+      info_.SetContent("Compression", EnumerationToString(job_.query_.GetCompression()));
+    }
+
+    virtual StateUpdate* Step()
+    {
+      Json::FastWriter writer;
+      const std::string lookup = writer.write(job_.query_.GetResources()); 
+
+      Json::Value answer;
+      if (!job_.peers_.DoPost(answer, job_.peerIndex_, URI_LOOKUP, lookup))
+      {
+        LOG(ERROR) << "Cannot retrieve the list of instances to pull from peer \"" 
+                   << job_.query_.GetPeer()
+                   << "\" (check that it has the transfers accelerator plugin installed)";
+        return StateUpdate::Failure();
+      } 
+
+      if (answer.type() != Json::objectValue ||
+          !answer.isMember(KEY_INSTANCES) ||
+          !answer.isMember(KEY_ORIGINATOR_UUID) ||
+          answer[KEY_INSTANCES].type() != Json::arrayValue ||
+          answer[KEY_ORIGINATOR_UUID].type() != Json::stringValue)
+      {
+        LOG(ERROR) << "Bad network protocol from peer: " << job_.query_.GetPeer();
+        return StateUpdate::Failure();
+      }
+
+      if (job_.query_.HasOriginator() &&
+          job_.query_.GetOriginator() != answer[KEY_ORIGINATOR_UUID].asString())
+      {
+        LOG(ERROR) << "Invalid originator, check out the \""
+                   << KEY_PLUGIN_CONFIGURATION << "." << KEY_BIDIRECTIONAL_PEERS
+                   << "\" configuration option";
+        return StateUpdate::Failure();
+      }
+
+      TransferScheduler  scheduler;
+
+      for (Json::Value::ArrayIndex i = 0; i < answer[KEY_INSTANCES].size(); i++)
+      {
+        DicomInstanceInfo instance(answer[KEY_INSTANCES][i]);
+        scheduler.AddInstance(instance);
+      }
+
+      if (scheduler.GetInstancesCount() == 0)
+      {
+        // We're already done: No instance to be retrieved
+        return StateUpdate::Success();
+      }
+      else
+      {
+        return StateUpdate::Next(new PullBucketsState(job_, info_, scheduler));
+      }
+    }
+
+    virtual void Stop(OrthancPluginJobStopReason reason)
+    {
+    }
+  };
+
+
+  StatefulOrthancJob::StateUpdate* PullJob::CreateInitialState(JobInfo& info)
+  {
+    return StateUpdate::Next(new LookupInstancesState(*this, info));
+  }
+    
+    
+  PullJob::PullJob(OrthancPluginContext* context,
+                   const TransferQuery& query,
+                   size_t threadsCount,
+                   size_t targetBucketSize) :
+    StatefulOrthancJob(JOB_TYPE_PULL),
+    context_(context),
+    query_(query),
+    threadsCount_(threadsCount),
+    targetBucketSize_(targetBucketSize),
+    peers_(context)
+  {
+    if (!peers_.LookupName(peerIndex_, query_.GetPeer()))
+    {
+      LOG(ERROR) << "Unknown Orthanc peer: " << query_.GetPeer();
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+
+    Json::Value serialized;
+    query.Serialize(serialized);
+    UpdateSerialized(serialized);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PullMode/PullJob.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,50 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../StatefulOrthancJob.h"
+#include "../TransferQuery.h"
+
+
+namespace OrthancPlugins
+{
+  class PullJob : public StatefulOrthancJob
+  {
+  private:
+    class LookupInstancesState;
+    class PullBucketsState;
+    class CommitState;
+
+    OrthancPluginContext  *context_;
+    TransferQuery          query_;
+    size_t                 threadsCount_;
+    size_t                 targetBucketSize_;
+    OrthancPeers           peers_;
+    size_t                 peerIndex_;
+
+    virtual StateUpdate* CreateInitialState(JobInfo& info);    
+    
+  public:
+    PullJob(OrthancPluginContext* context,
+            const TransferQuery& query,
+            size_t threadsCount,
+            size_t targetBucketSize);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PushMode/ActivePushTransactions.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,186 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ActivePushTransactions.h"
+
+#include "../DownloadArea.h"
+
+#include <Core/Logging.h>
+
+
+namespace OrthancPlugins
+{
+  class ActivePushTransactions::Transaction : public boost::noncopyable
+  {
+  private:
+    DownloadArea                 area_;
+    std::vector<TransferBucket>  buckets_;
+    BucketCompression            compression_;
+
+  public:
+    Transaction(const std::vector<DicomInstanceInfo>& instances,
+                const std::vector<TransferBucket>& buckets,
+                BucketCompression compression) :
+      area_(instances),
+      buckets_(buckets),
+      compression_(compression)
+    {
+    }
+
+    DownloadArea& GetDownloadArea()
+    {
+      return area_;
+    }
+
+    BucketCompression GetCompression() const
+    {
+      return compression_;
+    }
+
+    const TransferBucket& GetBucket(size_t index) const
+    {
+      if (index >= buckets_.size())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        return buckets_[index];
+      }
+    }
+
+    void Store(size_t bucketIndex,
+               const void* data,
+               size_t size)
+    {
+      area_.WriteBucket(GetBucket(bucketIndex), data, size, compression_);
+    }
+  };
+    
+
+  void ActivePushTransactions::FinalizeTransaction(OrthancPluginContext* context,
+                                                   const std::string& transactionUuid,
+                                                   bool commit)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    Content::iterator found = content_.find(transactionUuid);
+    if (found == content_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+
+    assert(found->second != NULL);
+    if (commit)
+    {
+      found->second->GetDownloadArea().Commit(context);
+    }
+
+    delete found->second;
+    content_.erase(found);
+    index_.Invalidate(transactionUuid);
+  }
+
+
+  ActivePushTransactions::~ActivePushTransactions()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      LOG(WARNING) << "Discarding an uncommitted push transaction "
+                   << "in the transfers accelerator: " << it->first;
+        
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+    
+
+  void ActivePushTransactions::ListTransactions(std::vector<std::string>& target)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    target.clear();
+    target.reserve(content_.size());
+
+    for (Content::const_iterator it = content_.begin();
+         it != content_.end(); ++it)
+    {
+      target.push_back(it->first);
+    }
+  }
+
+  
+  std::string ActivePushTransactions::CreateTransaction(const std::vector<DicomInstanceInfo>& instances,
+                                                        const std::vector<TransferBucket>& buckets,
+                                                        BucketCompression compression)
+  {
+    std::string uuid = Orthanc::Toolbox::GenerateUuid();
+    std::auto_ptr<Transaction> tmp(new Transaction(instances, buckets, compression));
+
+    LOG(INFO) << "Creating transaction to receive " << instances.size()
+              << " instances (" << ConvertToMegabytes(tmp->GetDownloadArea().GetTotalSize())
+              << "MB) in push mode: " << uuid;
+      
+    {
+      boost::mutex::scoped_lock  lock(mutex_);
+
+      // Drop the oldest active transaction, if not enough place
+      if (content_.size() == maxSize_)
+      {
+        std::string oldest = index_.RemoveOldest();
+
+        Content::iterator transaction = content_.find(oldest);
+        assert(transaction != content_.end() &&
+               transaction->second != NULL);
+
+        delete transaction->second;
+        content_.erase(transaction);
+
+        LOG(WARNING) << "An inactive push transaction has been discarded: " << oldest;
+      }
+
+      index_.Add(uuid);
+      content_[uuid] = tmp.release();
+    }
+
+    return uuid;
+  }
+    
+
+  void ActivePushTransactions::Store(OrthancPluginContext* context,
+                                     const std::string& transactionUuid,
+                                     size_t bucketIndex,
+                                     const void* data,
+                                     size_t size)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    Content::iterator found = content_.find(transactionUuid);
+    if (found == content_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+      
+    assert(found->second != NULL);
+
+    index_.MakeMostRecent(transactionUuid);
+      
+    found->second->Store(bucketIndex, data, size);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PushMode/ActivePushTransactions.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,79 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../TransferBucket.h"
+
+#include <orthanc/OrthancCPlugin.h>
+#include <Core/Cache/LeastRecentlyUsedIndex.h>
+
+#include <boost/thread/mutex.hpp>
+
+namespace OrthancPlugins
+{
+  class ActivePushTransactions : public boost::noncopyable
+  {
+  private:
+    class Transaction;
+    
+    typedef Orthanc::LeastRecentlyUsedIndex<std::string>  Index;
+    typedef std::map<std::string, Transaction*>           Content;
+
+    boost::mutex  mutex_;
+    Content       content_;
+    Index         index_;
+    size_t        maxSize_;
+
+    void FinalizeTransaction(OrthancPluginContext* context,
+                             const std::string& transactionUuid,
+                             bool commit);
+
+  public:
+    ActivePushTransactions(size_t maxSize) :
+      maxSize_(maxSize)
+    {
+    }
+
+    ~ActivePushTransactions();
+    
+    void ListTransactions(std::vector<std::string>& target);
+
+    std::string CreateTransaction(const std::vector<DicomInstanceInfo>& instances,
+                                  const std::vector<TransferBucket>& buckets,
+                                  BucketCompression compression);
+
+    void Store(OrthancPluginContext* context,
+               const std::string& transactionUuid,
+               size_t bucketIndex,
+               const void* data,
+               size_t size);
+
+    void Commit(OrthancPluginContext* context,
+                const std::string& transactionUuid)
+    {
+      FinalizeTransaction(context, transactionUuid, true);
+    }
+
+    void Discard(const std::string& transactionUuid)
+    {
+      FinalizeTransaction(NULL, transactionUuid, false);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PushMode/BucketPushQuery.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,84 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "BucketPushQuery.h"
+
+#include <Core/ChunkedBuffer.h>
+#include <Core/Compression/GzipCompressor.h>
+
+#include <boost/lexical_cast.hpp>
+
+
+namespace OrthancPlugins
+{
+  BucketPushQuery::BucketPushQuery(OrthancInstancesCache& cache,
+                                   const TransferBucket& bucket,
+                                   const std::string& peer,
+                                   const std::string& transactionUri,
+                                   size_t bucketIndex,
+                                   BucketCompression compression) :
+    cache_(cache),
+    bucket_(bucket),
+    peer_(peer),
+    compression_(compression)
+  {
+    uri_ = transactionUri + "/" + boost::lexical_cast<std::string>(bucketIndex);
+  }
+
+
+  void BucketPushQuery::ReadBody(std::string& body) const
+  {
+    Orthanc::ChunkedBuffer buffer;
+
+    for (size_t j = 0; j < bucket_.GetChunksCount(); j++)
+    {
+      std::string chunk;
+      std::string md5;  // unused
+      cache_.GetChunk(chunk, md5, bucket_, j);
+      buffer.AddChunk(chunk);
+    }
+
+    switch (compression_)
+    {
+      case BucketCompression_None:
+        buffer.Flatten(body);
+        break;
+
+      case BucketCompression_Gzip:
+      {
+        std::string raw;
+        buffer.Flatten(raw);
+            
+        Orthanc::GzipCompressor compressor;
+        Orthanc::IBufferCompressor::Compress(body, compressor, raw);
+        break;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  
+  void BucketPushQuery::HandleAnswer(const void* answer,
+                                     size_t size)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PushMode/BucketPushQuery.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,64 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../HttpQueries/IHttpQuery.h"
+#include "../OrthancInstancesCache.h"
+
+namespace OrthancPlugins
+{
+  class BucketPushQuery : public IHttpQuery
+  {
+  private:
+    OrthancInstancesCache&  cache_;
+    TransferBucket          bucket_;
+    std::string             peer_;
+    std::string             uri_;
+    BucketCompression       compression_;
+
+  public:
+    BucketPushQuery(OrthancInstancesCache& cache,
+                    const TransferBucket& bucket,
+                    const std::string& peer,
+                    const std::string& transactionUri,
+                    size_t bucketIndex,
+                    BucketCompression compression);
+
+    virtual Orthanc::HttpMethod GetMethod() const
+    {
+      return Orthanc::HttpMethod_Put;
+    }
+
+    virtual const std::string& GetPeer() const
+    {
+      return peer_;
+    }
+
+    virtual const std::string& GetUri() const
+    {
+      return uri_;
+    }
+
+    virtual void ReadBody(std::string& body) const;
+
+    virtual void HandleAnswer(const void* answer,
+                              size_t size);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PushMode/PushJob.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,272 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PushJob.h"
+
+#include "BucketPushQuery.h"
+#include "../HttpQueries/HttpQueriesRunner.h"
+#include "../TransferScheduler.h"
+
+#include <Core/Logging.h>
+
+#include <json/writer.h>
+
+
+namespace OrthancPlugins
+{
+  class PushJob::FinalState : public IState
+  {
+  private:
+    const PushJob&  job_;
+    JobInfo&        info_;
+    std::string     transactionUri_;
+    bool            isCommit_;
+      
+  public:
+    FinalState(const PushJob& job,
+               JobInfo& info,
+               const std::string& transactionUri,
+               bool isCommit) :
+      job_(job),
+      info_(info),
+      transactionUri_(transactionUri),
+      isCommit_(isCommit)
+    {
+    }
+
+    virtual StateUpdate* Step()
+    {
+      std::string uri = transactionUri_;
+        
+      if (isCommit_)
+      {
+        uri += "/commit";
+      }
+      else
+      {
+        uri += "/discard";
+      }
+        
+      Json::Value answer;
+      if (!job_.peers_.DoPost(answer, job_.peerIndex_, uri, ""))
+      {
+        if (isCommit_)
+        {
+          LOG(ERROR) << "Cannot commit push transaction on remote peer: "
+                     << job_.query_.GetPeer();
+        }
+          
+        return StateUpdate::Failure();
+      } 
+      else if (isCommit_)
+      {
+        return StateUpdate::Success();
+      }
+      else
+      {
+        return StateUpdate::Failure();
+      }
+    }
+
+    virtual void Stop(OrthancPluginJobStopReason reason)
+    {
+    }
+  };
+
+
+  class PushJob::PushBucketsState : public IState
+  {
+  private:
+    const PushJob&                    job_;
+    JobInfo&                          info_;
+    std::string                       transactionUri_;
+    HttpQueriesQueue                  queue_;
+    std::auto_ptr<HttpQueriesRunner>  runner_;
+
+    void UpdateInfo()
+    {
+      size_t scheduledQueriesCount, completedQueriesCount;
+      uint64_t uploadedSize, downloadedSize;
+      queue_.GetStatistics(scheduledQueriesCount, completedQueriesCount, downloadedSize, uploadedSize);
+
+      info_.SetContent("UploadedSizeMB", ConvertToMegabytes(uploadedSize));
+      info_.SetContent("CompletedHttpQueries", static_cast<unsigned int>(completedQueriesCount));
+
+      if (runner_.get() != NULL)
+      {
+        float speed;
+        runner_->GetSpeed(speed);
+        info_.SetContent("NetworkSpeedKBs", static_cast<unsigned int>(speed));
+      }
+            
+      // The "2" below corresponds to the "CreateTransactionState"
+      // and "FinalState" steps (which prevents division by zero)
+      info_.SetProgress(static_cast<float>(1 /* CreateTransactionState */ + completedQueriesCount) / 
+                        static_cast<float>(2 + scheduledQueriesCount));
+    }
+
+  public:
+    PushBucketsState(const PushJob&  job,
+                     JobInfo& info,
+                     const std::string& transactionUri,
+                     const std::vector<TransferBucket>& buckets) :
+      job_(job),
+      info_(info),
+      transactionUri_(transactionUri),
+      queue_(job.context_)
+    {
+      queue_.Reserve(buckets.size());
+        
+      for (size_t i = 0; i < buckets.size(); i++)
+      {
+        queue_.Enqueue(new BucketPushQuery(job.cache_, buckets[i], job.query_.GetPeer(),
+                                           transactionUri_, i, job.query_.GetCompression()));
+      }
+
+      UpdateInfo();
+    }
+      
+    virtual StateUpdate* Step()
+    {
+      if (runner_.get() == NULL)
+      {
+        runner_.reset(new HttpQueriesRunner(queue_, job_.threadsCount_));
+      }
+
+      HttpQueriesQueue::Status status = queue_.WaitComplete(200);
+
+      UpdateInfo();
+
+      switch (status)
+      {
+        case HttpQueriesQueue::Status_Running:
+          return StateUpdate::Continue();
+
+        case HttpQueriesQueue::Status_Success:
+          // Commit transaction on remote peer
+          return StateUpdate::Next(new FinalState(job_, info_, transactionUri_, true));
+
+        case HttpQueriesQueue::Status_Failure:
+          // Discard transaction on remote peer
+          return StateUpdate::Next(new FinalState(job_, info_, transactionUri_, false));
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }        
+    }
+
+    virtual void Stop(OrthancPluginJobStopReason reason)
+    {
+      // Cancel the running download threads
+      runner_.reset();
+    }
+  };
+
+
+  class PushJob::CreateTransactionState : public IState
+  {
+  private:
+    const PushJob&                job_;
+    JobInfo&                      info_;
+    std::string                   createTransaction_;
+    std::vector<TransferBucket>   buckets_;
+
+  public:
+    CreateTransactionState(const PushJob& job,
+                           JobInfo& info) :
+      job_(job),
+      info_(info)
+    {
+      TransferScheduler scheduler;
+      scheduler.ParseListOfResources(job_.cache_, job_.query_.GetResources());
+
+      Json::Value push;      
+      scheduler.FormatPushTransaction(push, buckets_,
+                                      job.targetBucketSize_, 2 * job.targetBucketSize_,
+                                      job_.query_.GetCompression());
+
+      Json::FastWriter writer;
+      createTransaction_ = writer.write(push);
+
+      info_.SetContent("Peer", job_.query_.GetPeer());
+      info_.SetContent("Compression", EnumerationToString(job_.query_.GetCompression()));
+      info_.SetContent("TotalInstances", static_cast<unsigned int>(scheduler.GetInstancesCount()));
+      info_.SetContent("TotalSizeMB", ConvertToMegabytes(scheduler.GetTotalSize()));
+    }
+
+    virtual StateUpdate* Step()
+    {
+      Json::Value answer;
+      if (!job_.peers_.DoPost(answer, job_.peerIndex_, URI_PUSH, createTransaction_))
+      {
+        LOG(ERROR) << "Cannot create a push transaction to peer \"" 
+                   << job_.query_.GetPeer()
+                   << "\" (check that it has the transfers accelerator plugin installed)";
+        return StateUpdate::Failure();
+      } 
+
+      if (answer.type() != Json::objectValue ||
+          !answer.isMember(KEY_PATH) ||
+          answer[KEY_PATH].type() != Json::stringValue)
+      {
+        LOG(ERROR) << "Bad network protocol from peer: " << job_.query_.GetPeer();
+        return StateUpdate::Failure();
+      }
+
+      std::string transactionUri = answer[KEY_PATH].asString();
+
+      return StateUpdate::Next(new PushBucketsState(job_, info_, transactionUri, buckets_));
+    }
+
+    virtual void Stop(OrthancPluginJobStopReason reason)
+    {
+    }
+  };
+
+
+  StatefulOrthancJob::StateUpdate* PushJob::CreateInitialState(JobInfo& info)
+  {
+    return StateUpdate::Next(new CreateTransactionState(*this, info));
+  }
+    
+    
+  PushJob::PushJob(OrthancPluginContext* context,
+                   const TransferQuery& query,
+                   OrthancInstancesCache& cache,
+                   size_t threadsCount,
+                   size_t targetBucketSize) :
+    StatefulOrthancJob(JOB_TYPE_PUSH),
+    context_(context),
+    cache_(cache),
+    query_(query),
+    threadsCount_(threadsCount),
+    targetBucketSize_(targetBucketSize),
+    peers_(context)
+  {
+    if (!peers_.LookupName(peerIndex_, query_.GetPeer()))
+    {
+      LOG(ERROR) << "Unknown Orthanc peer: " << query_.GetPeer();
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+
+    Json::Value serialized;
+    query.Serialize(serialized);
+    UpdateSerialized(serialized);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PushMode/PushJob.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,52 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../OrthancInstancesCache.h"
+#include "../StatefulOrthancJob.h"
+#include "../TransferQuery.h"
+
+namespace OrthancPlugins
+{
+  class PushJob : public StatefulOrthancJob
+  {
+  private:
+    class CreateTransactionState;    
+    class PushBucketsState;
+    class FinalState;
+
+    OrthancPluginContext    *context_;
+    OrthancInstancesCache&   cache_;
+    TransferQuery            query_;
+    size_t                   threadsCount_;
+    size_t                   targetBucketSize_;
+    OrthancPeers             peers_;
+    size_t                   peerIndex_;
+    
+    virtual StateUpdate* CreateInitialState(JobInfo& info);
+    
+  public:
+    PushJob(OrthancPluginContext* context,
+            const TransferQuery& query,
+            OrthancInstancesCache& cache,
+            size_t threadsCount,
+            size_t targetBucketSize);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/SourceDicomInstance.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,72 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SourceDicomInstance.h"
+
+#include <Core/Logging.h>
+#include <Core/Toolbox.h>
+#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
+
+
+namespace OrthancPlugins
+{
+  SourceDicomInstance::SourceDicomInstance(OrthancPluginContext* context,
+                                           const std::string& instanceId) :
+    context_(context)
+  {
+    LOG(INFO) << "Transfers accelerator reading DICOM instance: " << instanceId;
+      
+    MemoryBuffer buffer(context);
+    buffer.GetDicomInstance(instanceId);
+
+    info_.reset(new DicomInstanceInfo(instanceId, buffer));
+
+    buffer_ = buffer.Release();
+  }
+
+  
+  SourceDicomInstance::~SourceDicomInstance()
+  {
+    OrthancPluginFreeMemoryBuffer(context_, &buffer_);
+  }
+
+
+  const DicomInstanceInfo& SourceDicomInstance::GetInfo() const
+  {
+    assert(info_.get() != NULL);
+    return *info_;
+  }
+
+
+  void SourceDicomInstance::GetChunk(std::string& target /* out */,
+                                     std::string& md5 /* out */,
+                                     size_t offset,
+                                     size_t size) const
+  {
+    if (offset + size > buffer_.size)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    const char* start = reinterpret_cast<const char*>(buffer_.data) + offset;
+    target.assign(start, start + size);
+
+    Orthanc::Toolbox::ComputeMD5(md5, start, size);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/SourceDicomInstance.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,56 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomInstanceInfo.h"
+
+#include <orthanc/OrthancCPlugin.h>
+
+#include <boost/noncopyable.hpp>
+#include <memory>
+
+namespace OrthancPlugins
+{
+  class SourceDicomInstance : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*             context_;
+    OrthancPluginMemoryBuffer         buffer_;
+    std::auto_ptr<DicomInstanceInfo>  info_;
+
+  public:
+    SourceDicomInstance(OrthancPluginContext* context,
+                        const std::string& instanceId);
+
+    ~SourceDicomInstance();
+
+    const void* GetBuffer() const
+    {
+      return buffer_.data;
+    }
+
+    const DicomInstanceInfo& GetInfo() const;
+
+    void GetChunk(std::string& target /* out */,
+                  std::string& md5 /* out */,
+                  size_t offset,
+                  size_t size) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/StatefulOrthancJob.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,153 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "StatefulOrthancJob.h"
+
+namespace OrthancPlugins
+{
+  StatefulOrthancJob::JobInfo::JobInfo(StatefulOrthancJob& job) :
+    job_(job),
+    updated_(true),
+    content_(Json::objectValue)
+  {
+  }
+
+
+  void StatefulOrthancJob::JobInfo::SetContent(const std::string& key,
+                                               const Json::Value& value)
+  {
+    content_[key] = value;
+    updated_ = true;
+  }
+
+  
+  void StatefulOrthancJob::JobInfo::Update()
+  {
+    if (updated_)
+    {
+      job_.UpdateContent(content_);
+      updated_ = false;
+    }
+  }
+    
+
+  StatefulOrthancJob::StateUpdate::StateUpdate(IState* state) :
+    status_(OrthancPluginJobStepStatus_Continue),
+    state_(state)
+  {
+    if (state == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+
+  StatefulOrthancJob::IState* StatefulOrthancJob::StateUpdate::ReleaseOtherState()
+  {
+    if (state_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return state_.release();
+    }
+  }
+
+
+  StatefulOrthancJob::StatefulOrthancJob(const std::string& jobType) :
+    OrthancJob(jobType),
+    info_(*this)
+  {
+  }
+
+  
+  OrthancPluginJobStepStatus StatefulOrthancJob::Step()
+  {
+    std::auto_ptr<StateUpdate> update;
+
+    if (state_.get() == NULL)
+    {        
+      update.reset(CreateInitialState(info_));
+    }
+    else
+    {
+      update.reset(state_->Step());
+    }
+
+    info_.Update();
+
+    if (update.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    if (update->IsOtherState())
+    {
+      state_.reset(update->ReleaseOtherState());
+      assert(state_.get() != NULL);
+
+      return OrthancPluginJobStepStatus_Continue;
+    }
+    else
+    {
+      OrthancPluginJobStepStatus status = update->GetStatus();
+
+      if (status == OrthancPluginJobStepStatus_Success)
+      {
+        info_.SetProgress(1);
+      }
+
+      if (status == OrthancPluginJobStepStatus_Success ||
+          status == OrthancPluginJobStepStatus_Failure)
+      {
+        state_.reset();
+      }
+
+      return status;
+    }
+  }
+
+  
+  void StatefulOrthancJob::Stop(OrthancPluginJobStopReason reason)
+  {
+    if (state_.get() != NULL)
+    {
+      state_->Stop(reason);
+
+      if (reason != OrthancPluginJobStopReason_Paused)
+      {
+        // Drop the current state, so as to force going back to the
+        // initial state on resubmit
+        state_.reset();
+      }
+    }
+  }
+
+  
+  void StatefulOrthancJob::Reset()
+  {
+    if (state_.get() != NULL)
+    {
+      // Error in the Orthanc core: Reset() should only be called
+      // from the "Failure" state, where no "IState*" is available
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/StatefulOrthancJob.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,136 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
+
+#include <memory>
+
+
+namespace OrthancPlugins
+{
+  class StatefulOrthancJob : public OrthancJob
+  {
+  protected:
+    class IState;
+
+
+    class JobInfo : public boost::noncopyable
+    {
+    private:
+      StatefulOrthancJob&  job_;
+      bool                 updated_;
+      Json::Value          content_;
+
+    public:
+      JobInfo(StatefulOrthancJob& job);
+
+      void SetProgress(float progress)
+      {
+        job_.UpdateProgress(progress);
+      }
+
+      void SetContent(const std::string& key,
+                      const Json::Value& value);
+
+      void Update();
+    };
+    
+
+    
+    class StateUpdate : public boost::noncopyable
+    {
+    private:
+      OrthancPluginJobStepStatus  status_;
+      std::auto_ptr<IState>       state_;
+
+      StateUpdate(OrthancPluginJobStepStatus status) :
+        status_(status)
+      {
+      }
+
+      StateUpdate(IState* state);
+
+    public:
+      static StateUpdate* Next(IState* state)  // Takes ownsership
+      {
+        return new StateUpdate(state);
+      }
+
+      static StateUpdate* Continue()
+      {
+        return new StateUpdate(OrthancPluginJobStepStatus_Continue);
+      }
+
+      static StateUpdate* Success()
+      {
+        return new StateUpdate(OrthancPluginJobStepStatus_Success);
+      }
+
+      static StateUpdate* Failure()
+      {
+        return new StateUpdate(OrthancPluginJobStepStatus_Failure);
+      }
+
+      OrthancPluginJobStepStatus GetStatus() const
+      {
+        return status_;
+      }
+      
+      bool IsOtherState() const
+      {
+        return state_.get() != NULL;
+      }
+
+      IState* ReleaseOtherState();
+    };    
+
+
+    class IState : public boost::noncopyable
+    {
+    public:
+      virtual ~IState()
+      {
+      }
+
+      virtual StateUpdate* Step() = 0;
+
+      virtual void Stop(OrthancPluginJobStopReason reason) = 0;
+    };
+
+    
+    virtual StateUpdate* CreateInitialState(JobInfo& info) = 0;
+    
+
+  private:
+    std::auto_ptr<IState>   state_;
+    JobInfo                 info_;
+
+
+  public:
+    StatefulOrthancJob(const std::string& jobType);
+    
+    virtual OrthancPluginJobStepStatus Step();
+
+    virtual void Stop(OrthancPluginJobStopReason reason);
+    
+    virtual void Reset();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/TransferBucket.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,229 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "TransferBucket.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+
+namespace OrthancPlugins
+{
+  TransferBucket::TransferBucket() :
+    totalSize_(0),
+    extensible_(true)
+  {
+  }
+
+    
+  TransferBucket::TransferBucket(const Json::Value& serialized) :
+    totalSize_(0),
+    extensible_(false)
+  {
+    if (serialized.type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    chunks_.reserve(serialized.size());
+
+    for (Json::Value::ArrayIndex i = 0; i < serialized.size(); i++)
+    {
+      if (serialized[i].type() != Json::objectValue ||
+          !serialized[i].isMember(KEY_ID) ||
+          !serialized[i].isMember(KEY_OFFSET) ||
+          !serialized[i].isMember(KEY_SIZE) ||
+          serialized[i][KEY_ID].type() != Json::stringValue ||
+          serialized[i][KEY_OFFSET].type() != Json::stringValue ||
+          serialized[i][KEY_SIZE].type() != Json::stringValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        try
+        {
+          Chunk chunk;
+          chunk.instanceId_ = serialized[i][KEY_ID].asString();
+          chunk.offset_ = boost::lexical_cast<size_t>(serialized[i][KEY_OFFSET].asString());
+          chunk.size_ = boost::lexical_cast<size_t>(serialized[i][KEY_SIZE].asString());
+
+          chunks_.push_back(chunk);
+          totalSize_ += chunk.size_;
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+      }
+    }
+  }
+
+
+  void TransferBucket::Serialize(Json::Value& target) const
+  {
+    target = Json::arrayValue;
+
+    for (size_t i = 0; i < chunks_.size(); i++)
+    {
+      Json::Value item = Json::objectValue;
+      item[KEY_ID] = chunks_[i].instanceId_;
+      item[KEY_OFFSET] = boost::lexical_cast<std::string>(chunks_[i].offset_);
+      item[KEY_SIZE] = boost::lexical_cast<std::string>(chunks_[i].size_);
+      target.append(item);
+    }
+  }
+    
+  void TransferBucket::Clear()
+  {
+    chunks_.clear();
+    totalSize_ = 0;
+    extensible_ = true;
+  }
+    
+    
+  void TransferBucket::AddChunk(const DicomInstanceInfo& instance,
+                                size_t chunkOffset,
+                                size_t chunkSize)
+  {
+    if (chunkOffset + chunkSize > instance.GetSize())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!extensible_)
+    {
+      LOG(ERROR) << "Cannot add a new chunk after a truncated instance";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+      
+    if (!chunks_.empty() &&
+        chunkOffset != 0)
+    {
+      LOG(ERROR) << "Only the first chunk can have non-zero offset in a transfer bucket";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    if (chunkSize == 0)
+    {
+      // Ignore empty chunks
+      return;
+    }
+
+    if (!chunks_.empty() &&
+        chunkSize != instance.GetSize())
+    {
+      // Prevents adding new chunk after an incomplete instance
+      extensible_ = false;
+    }
+
+    Chunk chunk;
+    chunk.instanceId_ = instance.GetId();
+    chunk.offset_ = chunkOffset;
+    chunk.size_ = chunkSize;
+
+    chunks_.push_back(chunk);
+    totalSize_ += chunkSize;
+  }
+    
+
+  const std::string& TransferBucket::GetChunkInstanceId(size_t index) const
+  {
+    if (index >= chunks_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return chunks_[index].instanceId_;
+    }
+  }
+
+  
+  size_t TransferBucket::GetChunkOffset(size_t index) const
+  {
+    if (index >= chunks_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return chunks_[index].offset_;
+    }
+  }
+
+  
+  size_t TransferBucket::GetChunkSize(size_t index) const
+  {
+    if (index >= chunks_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return chunks_[index].size_;
+    }
+  }
+
+  
+  void TransferBucket::ComputePullUri(std::string& uri,
+                                      BucketCompression compression) const
+  {
+    if (chunks_.empty())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    bool first = true;
+    uri = std::string(URI_CHUNKS) + "/";
+
+    for (size_t i = 0; i < chunks_.size(); i++)
+    {
+      if (first)
+      {
+        first = false;
+      }
+      else
+      {
+        uri += ".";
+      }
+
+      uri += chunks_[i].instanceId_;
+
+      assert(i == 0 || chunks_[i].offset_ == 0);
+    }
+
+    uri += ("?offset=" + boost::lexical_cast<std::string>(chunks_[0].offset_) +
+            "&size=" + boost::lexical_cast<std::string>(totalSize_));
+
+    switch (compression)
+    {
+      case BucketCompression_None:
+        uri += "&compression=none";
+        break;
+
+      case BucketCompression_Gzip:
+        uri += "&compression=gzip";
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/TransferBucket.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,78 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomInstanceInfo.h"
+#include "TransferToolbox.h"
+
+namespace OrthancPlugins
+{
+  class TransferBucket
+  {
+  private:
+    struct Chunk
+    {
+      std::string  instanceId_;
+      size_t       offset_;
+      size_t       size_;
+    };
+
+    std::vector<Chunk>  chunks_;
+    size_t              totalSize_;
+    bool                extensible_;
+
+  public:
+    TransferBucket();
+
+    TransferBucket(const Json::Value& serialized);
+
+    size_t GetTotalSize() const
+    {
+      return totalSize_;
+    }
+
+    void Reserve(size_t size)
+    {
+      chunks_.reserve(size);
+    }
+    
+    size_t GetChunksCount() const
+    {
+      return chunks_.size();
+    }
+
+    void Serialize(Json::Value& target) const;
+    
+    void Clear();
+    
+    void AddChunk(const DicomInstanceInfo& instance,
+                  size_t chunkOffset,
+                  size_t chunkSize);
+    
+    const std::string& GetChunkInstanceId(size_t index) const;
+
+    size_t GetChunkOffset(size_t index) const;
+
+    size_t GetChunkSize(size_t index) const;
+
+    void ComputePullUri(std::string& uri,
+                        BucketCompression compression) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/TransferQuery.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,105 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "TransferQuery.h"
+
+#include <Core/OrthancException.h>
+
+
+namespace OrthancPlugins
+{
+  TransferQuery::TransferQuery(const Json::Value& body)
+  {
+    if (body.type() != Json::objectValue ||
+        !body.isMember(KEY_RESOURCES) ||
+        !body.isMember(KEY_PEER) ||
+        !body.isMember(KEY_COMPRESSION) ||
+        body[KEY_RESOURCES].type() != Json::arrayValue ||
+        body[KEY_PEER].type() != Json::stringValue ||
+        body[KEY_COMPRESSION].type() != Json::stringValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    peer_ = body[KEY_PEER].asString();
+    resources_ = body[KEY_RESOURCES];
+    compression_ = StringToBucketCompression(body[KEY_COMPRESSION].asString());
+
+    if (body.isMember(KEY_ORIGINATOR_UUID))
+    {
+      if (body[KEY_ORIGINATOR_UUID].type() != Json::stringValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+      else 
+      {
+        hasOriginator_ = true;
+        originator_ = body[KEY_ORIGINATOR_UUID].asString();
+      }
+    }
+    else
+    {
+      hasOriginator_ = false;
+    }
+
+    if (body.isMember(KEY_PRIORITY))
+    {
+      if (body[KEY_PRIORITY].type() != Json::intValue &&
+          body[KEY_PRIORITY].type() != Json::uintValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+      else 
+      {
+        priority_ = body[KEY_PRIORITY].asInt();
+      }
+    }
+    else
+    {
+      priority_ = 0;
+    }
+  }
+
+
+  const std::string& TransferQuery::GetOriginator() const
+  {
+    if (hasOriginator_)
+    {
+      return originator_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void TransferQuery::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+    target[KEY_PEER] = peer_;
+    target[KEY_RESOURCES] = resources_;
+    target[KEY_COMPRESSION] = EnumerationToString(compression_);
+      
+    if (hasOriginator_)
+    {
+      target[KEY_ORIGINATOR_UUID] = originator_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/TransferQuery.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,70 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "TransferToolbox.h"
+
+#include <json/value.h>
+
+namespace OrthancPlugins
+{
+  class TransferQuery
+  {
+  private:
+    std::string        peer_;
+    Json::Value        resources_;
+    BucketCompression  compression_;
+    bool               hasOriginator_;
+    std::string        originator_;
+    int                priority_;
+
+  public:
+    TransferQuery(const Json::Value& body);
+
+    const std::string& GetPeer() const
+    {
+      return peer_;
+    }
+
+    BucketCompression GetCompression() const
+    {
+      return compression_;
+    }
+
+    const Json::Value& GetResources() const
+    {
+      return resources_;
+    }
+
+    bool HasOriginator() const
+    {
+      return hasOriginator_;
+    }
+
+    const std::string& GetOriginator() const;
+
+    int GetPriority() const
+    {
+      return priority_;
+    }
+
+    void Serialize(Json::Value& target) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/TransferScheduler.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,347 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "TransferScheduler.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
+
+
+namespace OrthancPlugins
+{
+  void TransferScheduler::AddResource(OrthancInstancesCache& cache, 
+                                      Orthanc::ResourceType level,
+                                      const std::string& id)
+  {
+    Json::Value resource;
+
+    std::string base;
+    switch (level)
+    {
+      case Orthanc::ResourceType_Patient:
+        base = "patients";
+        break;
+
+      case Orthanc::ResourceType_Study:
+        base = "studies";
+        break;
+
+      case Orthanc::ResourceType_Series:
+        base = "series";
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    if (RestApiGet(resource, cache.GetContext(), "/" + base + "/" + id + "/instances", false))
+    {
+      if (resource.type() != Json::arrayValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      for (Json::Value::ArrayIndex i = 0; i < resource.size(); i++)
+      {
+        if (resource[i].type() != Json::objectValue ||
+            !resource[i].isMember(KEY_ID) ||
+            resource[i][KEY_ID].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        AddInstance(cache, resource[i][KEY_ID].asString());
+      }
+    }
+    else
+    {
+      std::string s = Orthanc::EnumerationToString(level);
+      Orthanc::Toolbox::ToLowerCase(s);
+      LOG(WARNING) << "Missing " << s << ": " << id;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+  }
+
+
+  void TransferScheduler::ComputeBucketsInternal(std::vector<TransferBucket>& target,
+                                                 size_t groupThreshold,
+                                                 size_t separateThreshold,
+                                                 const std::string& baseUrl,  /* only needed in pull mode */
+                                                 BucketCompression compression /* only needed in pull mode */) const
+  {
+    if (groupThreshold > separateThreshold ||
+        separateThreshold == 0)  // (*)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    target.clear();
+
+    std::list<std::string>  toGroup_;
+
+    for (Instances::const_iterator it = instances_.begin();
+         it != instances_.end(); ++it)
+    {
+      size_t size = it->second.GetSize();
+
+      if (size < groupThreshold)
+      {
+        toGroup_.push_back(it->first);
+      }
+      else if (size < separateThreshold)
+      {
+        // Send the whole instance as it is
+        TransferBucket bucket;
+        bucket.AddChunk(it->second, 0, size);
+        target.push_back(bucket);
+      }
+      else
+      {
+        // Divide this large instance as a set of chunks
+        size_t chunksCount;
+
+        if (size % separateThreshold == 0)
+        {
+          chunksCount = size / separateThreshold;
+        }
+        else
+        {
+          chunksCount = size / separateThreshold + 1;
+        }
+
+        assert(chunksCount != 0);  // This follows from (*)
+
+        size_t chunkSize = size / chunksCount;
+        size_t offset = 0;
+
+        for (size_t i = 0; i < chunksCount; i++, offset += chunkSize)
+        {
+          TransferBucket bucket;
+
+          if (i == chunksCount - 1)
+          {
+            // The last chunk must contain all the remaining bytes
+            // of the instance (correction of rounding effects)
+            bucket.AddChunk(it->second, offset, size - offset);
+          }
+          else
+          {
+            bucket.AddChunk(it->second, offset, chunkSize);
+          }
+
+          target.push_back(bucket);
+        }
+      }
+    }
+
+    // Grouping the remaining small instances, preventing the
+    // download URL from getting too long: "If you keep URLs under
+    // 2000 characters, they'll work in virtually any combination of
+    // client and server software."
+    // https://stackoverflow.com/a/417184/881731
+
+    static const size_t MAX_URL_LENGTH = 2000 - 44 /* size of an Orthanc identifier (SHA-1) */;
+
+    TransferBucket bucket;
+
+    for (std::list<std::string>::const_iterator it = toGroup_.begin();
+         it != toGroup_.end(); ++it)
+    {
+      Instances::const_iterator instance = instances_.find(*it);
+      assert(instance != instances_.end());
+        
+      bucket.AddChunk(instance->second, 0, instance->second.GetSize());
+        
+      bool full = (bucket.GetTotalSize() >= groupThreshold);
+        
+      if (!full && !baseUrl.empty())
+      {
+        std::string uri;
+        bucket.ComputePullUri(uri, compression);
+
+        std::string url = baseUrl + uri;
+        full = (url.length() >= MAX_URL_LENGTH);
+      }
+
+      if (full)
+      {
+        target.push_back(bucket);
+        bucket.Clear();
+      }
+    }
+
+    if (bucket.GetChunksCount() > 0)
+    {
+      target.push_back(bucket);
+    }
+  }
+
+
+  void TransferScheduler::AddInstance(OrthancInstancesCache& cache, 
+                                      const std::string& instanceId)
+  {
+    size_t size;
+    std::string md5;
+    cache.GetInstanceInfo(size, md5, instanceId);
+          
+    AddInstance(DicomInstanceInfo(instanceId, size, md5));
+  }
+    
+
+  void TransferScheduler::AddInstance(const DicomInstanceInfo& info)
+  {
+    instances_[info.GetId()] = info;
+  }
+
+    
+  void TransferScheduler::ParseListOfResources(OrthancInstancesCache& cache, 
+                                               const Json::Value& resources)
+  {
+    if (resources.type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
+    {
+      if (resources[i].type() != Json::objectValue ||
+          !resources[i].isMember(KEY_LEVEL) ||
+          !resources[i].isMember(KEY_ID) ||
+          resources[i][KEY_LEVEL].type() != Json::stringValue ||
+          resources[i][KEY_ID].type() != Json::stringValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        Orthanc::ResourceType level = Orthanc::StringToResourceType(resources[i][KEY_LEVEL].asCString());
+
+        switch (level)
+        {
+          case Orthanc::ResourceType_Patient:
+            AddPatient(cache, resources[i][KEY_ID].asString());
+            break;
+
+          case Orthanc::ResourceType_Study:
+            AddStudy(cache, resources[i][KEY_ID].asString());
+            break;
+
+          case Orthanc::ResourceType_Series:
+            AddSeries(cache, resources[i][KEY_ID].asString());
+            break;
+
+          case Orthanc::ResourceType_Instance:
+            AddInstance(cache, resources[i][KEY_ID].asString());
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+      }
+    }
+  }
+
+    
+  void TransferScheduler::ListInstances(std::vector<DicomInstanceInfo>& target) const
+  {
+    target.clear();
+    target.reserve(instances_.size());
+
+    for (Instances::const_iterator it = instances_.begin();
+         it != instances_.end(); ++it)
+    {
+      assert(it->first == it->second.GetId());
+      target.push_back(it->second);
+    }
+  }
+
+
+  size_t TransferScheduler::GetTotalSize() const
+  {
+    size_t size = 0;
+
+    for (Instances::const_iterator it = instances_.begin();
+         it != instances_.end(); ++it)
+    {
+      size += it->second.GetSize();
+    }
+
+    return size;
+  }
+
+
+  void TransferScheduler::ComputePullBuckets(std::vector<TransferBucket>& target,
+                                             size_t groupThreshold,
+                                             size_t separateThreshold,
+                                             const std::string& baseUrl,
+                                             BucketCompression compression) const
+  {
+    ComputeBucketsInternal(target, groupThreshold, separateThreshold, baseUrl, compression);
+  }
+
+
+  void TransferScheduler::FormatPushTransaction(Json::Value& target,
+                                                std::vector<TransferBucket>& buckets,
+                                                size_t groupThreshold,
+                                                size_t separateThreshold,
+                                                BucketCompression compression) const
+  {
+    ComputeBucketsInternal(buckets, groupThreshold, separateThreshold, "", BucketCompression_None);
+
+    target = Json::objectValue;
+
+    Json::Value tmp = Json::arrayValue;
+      
+    for (Instances::const_iterator it = instances_.begin();
+         it != instances_.end(); ++it)
+    {
+      Json::Value item;
+      it->second.Serialize(item);
+      tmp.append(item);
+    }
+
+    target[KEY_INSTANCES] = tmp;
+
+    tmp = Json::arrayValue;
+
+    for (size_t i = 0; i < buckets.size(); i++)
+    {
+      Json::Value item;
+      buckets[i].Serialize(item);
+      tmp.append(item);
+    }
+
+    target[KEY_BUCKETS] = tmp;
+
+    switch (compression)
+    {
+      case BucketCompression_Gzip: 
+        target[KEY_COMPRESSION] = "gzip";
+        break;
+
+      case BucketCompression_None: 
+        target[KEY_COMPRESSION] = "none";
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/TransferScheduler.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,93 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OrthancInstancesCache.h"
+
+
+namespace OrthancPlugins
+{
+  class TransferScheduler : public boost::noncopyable
+  {
+  private:
+    void AddResource(OrthancInstancesCache& cache, 
+                     Orthanc::ResourceType level,
+                     const std::string& id);
+
+    void ComputeBucketsInternal(std::vector<TransferBucket>& target,
+                                size_t groupThreshold,
+                                size_t separateThreshold,
+                                const std::string& baseUrl,  /* only needed in pull mode */
+                                BucketCompression compression /* only needed in pull mode */) const;
+
+    typedef std::map<std::string, DicomInstanceInfo>   Instances;
+
+    Instances    instances_;
+
+
+  public:
+    void AddPatient(OrthancInstancesCache& cache, 
+                    const std::string& patient)
+    {
+      AddResource(cache, Orthanc::ResourceType_Patient, patient);
+    }
+
+    void AddStudy(OrthancInstancesCache& cache, 
+                  const std::string& study)
+    {
+      AddResource(cache, Orthanc::ResourceType_Study, study);
+    }
+
+    void AddSeries(OrthancInstancesCache& cache, 
+                   const std::string& series)
+    {
+      AddResource(cache, Orthanc::ResourceType_Series, series);
+    }
+
+    void AddInstance(OrthancInstancesCache& cache, 
+                     const std::string& instanceId);
+
+    void AddInstance(const DicomInstanceInfo& info);
+
+    void ParseListOfResources(OrthancInstancesCache& cache, 
+                              const Json::Value& resources);
+
+    void ListInstances(std::vector<DicomInstanceInfo>& target) const;
+
+    size_t GetInstancesCount() const
+    {
+      return instances_.size();
+    }
+
+    size_t GetTotalSize() const;
+
+    void ComputePullBuckets(std::vector<TransferBucket>& target,
+                            size_t groupThreshold,
+                            size_t separateThreshold,
+                            const std::string& baseUrl,
+                            BucketCompression compression) const;
+
+    void FormatPushTransaction(Json::Value& target,
+                               std::vector<TransferBucket>& buckets,
+                               size_t groupThreshold,
+                               size_t separateThreshold,
+                               BucketCompression compression) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/TransferToolbox.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,76 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "TransferToolbox.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+#include <boost/math/special_functions/round.hpp>
+
+
+namespace OrthancPlugins
+{
+  unsigned int ConvertToMegabytes(uint64_t value)
+  {
+    return static_cast<unsigned int>
+      (boost::math::round(static_cast<float>(value) / static_cast<float>(MB)));
+  }
+
+
+  unsigned int ConvertToKilobytes(uint64_t value)
+  {
+    return static_cast<unsigned int>
+      (boost::math::round(static_cast<float>(value) / static_cast<float>(KB)));
+  }
+
+
+  BucketCompression StringToBucketCompression(const std::string& value)
+  {
+    if (value == "gzip")
+    {
+      return BucketCompression_Gzip;
+    }
+    else if (value == "none")
+    {
+      return BucketCompression_None;
+    }
+    else
+    {
+      LOG(ERROR) << "Valid compression methods are \"gzip\" and \"none\", but found: " << value;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(BucketCompression compression)
+  {
+    switch (compression)
+    {
+      case BucketCompression_Gzip:
+        return "gzip";
+
+      case BucketCompression_None:
+        return "none";
+        
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/TransferToolbox.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,75 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <stdint.h>
+#include <string>
+
+static const unsigned int KB = 1024;
+static const unsigned int MB = 1024 * 1024;
+
+static const char* const JOB_TYPE_PULL = "PullTransfer";
+static const char* const JOB_TYPE_PUSH = "PushTransfer";
+
+static const char* const PLUGIN_NAME = "transfers";
+
+static const char* const KEY_BIDIRECTIONAL_PEERS = "BidirectionalPeers";
+static const char* const KEY_BUCKETS = "Buckets";
+static const char* const KEY_COMPRESSION = "Compression";
+static const char* const KEY_ID = "ID";
+static const char* const KEY_INSTANCES = "Instances";
+static const char* const KEY_LEVEL = "Level";
+static const char* const KEY_OFFSET = "Offset";
+static const char* const KEY_ORIGINATOR_UUID = "Originator";
+static const char* const KEY_PATH = "Path";
+static const char* const KEY_PEER = "Peer";
+static const char* const KEY_PLUGIN_CONFIGURATION = "Transfers";
+static const char* const KEY_PRIORITY = "Priority";
+static const char* const KEY_REMOTE_JOB = "RemoteJob";
+static const char* const KEY_RESOURCES = "Resources";
+static const char* const KEY_SIZE = "Size";
+static const char* const KEY_URL = "URL";
+
+static const char* const URI_CHUNKS = "/transfers/chunks";
+static const char* const URI_JOBS = "/jobs";
+static const char* const URI_LOOKUP = "/transfers/lookup";
+static const char* const URI_PEERS = "/transfers/peers";
+static const char* const URI_PLUGINS = "/plugins";
+static const char* const URI_PULL = "/transfers/pull";
+static const char* const URI_PUSH = "/transfers/push";
+static const char* const URI_SEND = "/transfers/send";
+
+  
+namespace OrthancPlugins
+{
+  enum BucketCompression
+  {
+    BucketCompression_None,
+    BucketCompression_Gzip
+  };
+
+  unsigned int ConvertToMegabytes(uint64_t value);
+
+  unsigned int ConvertToKilobytes(uint64_t value);
+
+  BucketCompression StringToBucketCompression(const std::string& value);
+
+  const char* EnumerationToString(BucketCompression compression);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/NEWS	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,8 @@
+Pending changes in the mainline
+===============================
+
+
+2018-09-17
+----------
+
+* Initial release
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/Plugin.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,725 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "PluginContext.h"
+#include "../Framework/HttpQueries/DetectTransferPlugin.h"
+#include "../Framework/PullMode/PullJob.h"
+#include "../Framework/PushMode/PushJob.h"
+#include "../Framework/TransferScheduler.h"
+
+#include <EmbeddedResources.h>
+
+#include <Core/ChunkedBuffer.h>
+#include <Core/Compression/GzipCompressor.h>
+#include <Core/Logging.h>
+
+
+static bool DisplayPerformanceWarning()
+{
+  (void) DisplayPerformanceWarning;   // Disable warning about unused function
+  LOG(WARNING) << "Performance warning in transfers accelerator: "
+               << "Non-release build, runtime debug assertions are turned on";
+  return true;
+}
+
+
+static size_t ReadSizeArgument(const OrthancPluginHttpRequest* request,
+                               uint32_t index)
+{
+  std::string value(request->getValues[index]);
+
+  try
+  {
+    int tmp = boost::lexical_cast<int>(value);
+    if (tmp >= 0)
+    {
+      return static_cast<size_t>(tmp);
+    }
+  }
+  catch (boost::bad_lexical_cast&)
+  {
+  }
+
+  LOG(ERROR) << "The \"" << request->getKeys[index]
+             << "\" GET argument must be a positive integer: " << value;
+  throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
+}
+
+
+void ServeChunks(OrthancPluginRestOutput* output,
+                 const char* url,
+                 const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context.GetOrthanc(), output, "GET");
+    return;
+  }
+  
+  assert(request->groupsCount == 1);
+
+  std::vector<std::string> instances;
+  Orthanc::Toolbox::TokenizeString(instances, std::string(request->groups[0]), '.');
+
+  size_t offset = 0;
+  size_t requestedSize = 0;
+  OrthancPlugins::BucketCompression compression = OrthancPlugins::BucketCompression_None;
+
+  for (uint32_t i = 0; i < request->getCount; i++)
+  {
+    std::string key(request->getKeys[i]);
+
+    if (key == "offset")
+    {
+      offset = ReadSizeArgument(request, i);
+    }
+    else if (key == "size")
+    {
+      requestedSize = ReadSizeArgument(request, i);
+    }
+    else if (key == "compression")
+    {
+      compression = OrthancPlugins::StringToBucketCompression(request->getValues[i]);
+    }
+    else
+    {
+      LOG(INFO) << "Ignored GET argument: " << key;
+    }
+  }
+
+
+  // Limit the number of clients
+  Orthanc::Semaphore::Locker lock(context.GetSemaphore());
+
+  Orthanc::ChunkedBuffer buffer;
+
+  for (size_t i = 0; i < instances.size() && (requestedSize == 0 ||
+                                              buffer.GetNumBytes() < requestedSize); i++)
+  {
+    size_t instanceSize;
+    std::string md5;  // Ignored
+    context.GetCache().GetInstanceInfo(instanceSize, md5, instances[i]);
+
+    if (offset >= instanceSize)
+    {
+      offset -= instanceSize;
+    }
+    else
+    {
+      size_t toRead;
+      
+      if (requestedSize == 0)
+      {
+        toRead = instanceSize - offset;
+      }
+      else
+      {
+        toRead = requestedSize - buffer.GetNumBytes();
+
+        if (toRead > instanceSize - offset)
+        {
+          toRead = instanceSize - offset;
+        }
+      }
+
+      std::string chunk;
+      std::string md5;  // Ignored
+      context.GetCache().GetChunk(chunk, md5, instances[i], offset, toRead);
+        
+      buffer.AddChunk(chunk);
+      offset = 0;
+
+      assert(requestedSize == 0 ||
+             buffer.GetNumBytes() <= requestedSize);
+    }
+  }
+
+  std::string chunk;
+  buffer.Flatten(chunk);
+  
+
+  switch (compression)
+  {
+    case OrthancPlugins::BucketCompression_None:
+    {
+      OrthancPluginAnswerBuffer(context.GetOrthanc(), output, chunk.c_str(),
+                                chunk.size(), "application/octet-stream");
+      break;
+    }
+
+    case OrthancPlugins::BucketCompression_Gzip:
+    {
+      std::string compressed;
+      Orthanc::GzipCompressor gzip;
+      //gzip.SetCompressionLevel(9);
+      Orthanc::IBufferCompressor::Compress(compressed, gzip, chunk);
+      OrthancPluginAnswerBuffer(context.GetOrthanc(), output, compressed.c_str(),
+                                compressed.size(), "application/gzip");
+      break;
+    }
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+}
+
+
+
+static bool ParsePostBody(Json::Value& body,
+                          OrthancPluginRestOutput* output,
+                          const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  Json::Reader reader;
+
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context.GetOrthanc(), output, "POST");
+    return false;
+  }
+  else if (reader.parse(request->body, request->body + request->bodySize, body))
+  {
+    return true;
+  }
+  else
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+  }
+}
+
+
+void LookupInstances(OrthancPluginRestOutput* output,
+                     const char* url,
+                     const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  Json::Value resources;
+  if (!ParsePostBody(resources, output, request))
+  {
+    return;
+  }
+  
+  OrthancPlugins::TransferScheduler scheduler;
+  scheduler.ParseListOfResources(context.GetCache(), resources);
+
+  Json::Value answer = Json::objectValue;
+  answer[KEY_INSTANCES] = Json::arrayValue;
+  answer[KEY_ORIGINATOR_UUID] = context.GetPluginUuid();
+  answer["CountInstances"] = static_cast<uint32_t>(scheduler.GetInstancesCount());
+  answer["TotalSize"] = boost::lexical_cast<std::string>(scheduler.GetTotalSize());
+  answer["TotalSizeMB"] = OrthancPlugins::ConvertToMegabytes(scheduler.GetTotalSize());
+
+  std::vector<OrthancPlugins::DicomInstanceInfo> instances;
+  scheduler.ListInstances(instances);
+
+  for (size_t i = 0; i < instances.size(); i++)
+  {
+    Json::Value instance;
+    instances[i].Serialize(instance);
+    answer[KEY_INSTANCES].append(instance);
+  }
+  
+  Json::FastWriter writer;
+  std::string s = writer.write(answer);
+  
+  OrthancPluginAnswerBuffer(context.GetOrthanc(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+
+static void SubmitJob(OrthancPluginRestOutput* output,
+                      OrthancPlugins::OrthancJob* job,
+                      int priority)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  std::string id = OrthancPlugins::OrthancJob::Submit(context.GetOrthanc(), job, priority);
+
+  Json::Value result = Json::objectValue;
+  result[KEY_ID] = id;
+  result[KEY_PATH] = std::string(URI_JOBS) + "/" + id;
+
+  std::string s = result.toStyledString();
+  OrthancPluginAnswerBuffer(context.GetOrthanc(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+
+void SchedulePull(OrthancPluginRestOutput* output,
+                  const char* url,
+                  const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  Json::Value body;
+  if (!ParsePostBody(body, output, request))
+  {
+    return;
+  }
+
+  OrthancPlugins::TransferQuery query(body);
+
+  SubmitJob(output, new OrthancPlugins::PullJob(context.GetOrthanc(), query,
+                                                context.GetThreadsCount(),
+                                                context.GetTargetBucketSize()),
+            query.GetPriority());
+}
+
+
+
+void CreatePush(OrthancPluginRestOutput* output,
+                const char* url,
+                const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  Json::Value query;
+  if (!ParsePostBody(query, output, request))
+  {
+    return;
+  }
+  
+  if (query.type() != Json::objectValue ||
+      !query.isMember(KEY_BUCKETS) ||
+      !query.isMember(KEY_COMPRESSION) ||
+      !query.isMember(KEY_INSTANCES) ||
+      query[KEY_BUCKETS].type() != Json::arrayValue ||
+      query[KEY_COMPRESSION].type() != Json::stringValue ||
+      query[KEY_INSTANCES].type() != Json::arrayValue)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+  }
+
+  std::vector<OrthancPlugins::DicomInstanceInfo> instances;
+  instances.reserve(query[KEY_INSTANCES].size());
+  
+  for (Json::Value::ArrayIndex i = 0; i < query[KEY_INSTANCES].size(); i++)
+  {
+    OrthancPlugins::DicomInstanceInfo instance(query[KEY_INSTANCES][i]);
+    instances.push_back(instance);
+  }
+
+  std::vector<OrthancPlugins::TransferBucket> buckets;
+  buckets.reserve(query[KEY_BUCKETS].size());
+  
+  for (Json::Value::ArrayIndex i = 0; i < query[KEY_BUCKETS].size(); i++)
+  {
+    OrthancPlugins::TransferBucket bucket(query[KEY_BUCKETS][i]);
+    buckets.push_back(bucket);
+  }
+
+  OrthancPlugins::BucketCompression compression =
+    OrthancPlugins::StringToBucketCompression(query[KEY_COMPRESSION].asString());
+                                              
+  std::string id = context.GetActivePushTransactions().CreateTransaction
+    (instances, buckets, compression);
+  
+  Json::Value result = Json::objectValue;
+  result[KEY_ID] = id;
+  result[KEY_PATH] = std::string(URI_PUSH) + "/" + id;
+
+  std::string s = result.toStyledString();  
+  OrthancPluginAnswerBuffer(context.GetOrthanc(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+void StorePush(OrthancPluginRestOutput* output,
+               const char* url,
+               const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  if (request->method != OrthancPluginHttpMethod_Put)
+  {
+    OrthancPluginSendMethodNotAllowed(context.GetOrthanc(), output, "PUT");
+    return;
+  }
+
+  assert(request->groupsCount == 2);
+  std::string transaction(request->groups[0]);
+  std::string chunk(request->groups[1]);
+
+  size_t chunkIndex;
+  
+  try
+  {
+    chunkIndex = boost::lexical_cast<size_t>(chunk);
+  }
+  catch (boost::bad_lexical_cast&)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+  }
+
+  context.GetActivePushTransactions().Store
+    (context.GetOrthanc(), transaction, chunkIndex, request->body, request->bodySize);
+
+  std::string s = "{}";
+  OrthancPluginAnswerBuffer(context.GetOrthanc(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+void CommitPush(OrthancPluginRestOutput* output,
+                const char* url,
+                const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context.GetOrthanc(), output, "POST");
+    return;
+  }
+
+  assert(request->groupsCount == 1);
+  std::string transaction(request->groups[0]);
+
+  context.
+    GetActivePushTransactions().Commit(context.GetOrthanc(), transaction);
+
+  std::string s = "{}";
+  OrthancPluginAnswerBuffer(context.GetOrthanc(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+void DiscardPush(OrthancPluginRestOutput* output,
+                 const char* url,
+                 const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  if (request->method != OrthancPluginHttpMethod_Delete)
+  {
+    OrthancPluginSendMethodNotAllowed(context.GetOrthanc(), output, "DELETE");
+    return;
+  }
+
+  assert(request->groupsCount == 1);
+  std::string transaction(request->groups[0]);
+
+  context.
+    GetActivePushTransactions().Discard(transaction);
+
+  std::string s = "{}";
+  OrthancPluginAnswerBuffer(context.GetOrthanc(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+
+void ScheduleSend(OrthancPluginRestOutput* output,
+                  const char* url,
+                  const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  Json::Value body;
+  if (!ParsePostBody(body, output, request))
+  {
+    return;
+  }
+
+  OrthancPlugins::TransferQuery query(body);
+
+  std::string remoteSelf;  // For pull mode
+  bool pullMode = context.LookupBidirectionalPeer(remoteSelf, query.GetPeer());
+
+  LOG(INFO) << "Sending resources to peer \"" << query.GetPeer() << "\" using "
+            << (pullMode ? "pull" : "push") << " mode";
+
+  if (pullMode)
+  {
+    OrthancPlugins::OrthancPeers peers(context.GetOrthanc());
+
+    Json::Value lookup = Json::objectValue;
+    lookup[KEY_RESOURCES] = query.GetResources();
+    lookup[KEY_COMPRESSION] = OrthancPlugins::EnumerationToString(query.GetCompression());
+    lookup[KEY_ORIGINATOR_UUID] = context.GetPluginUuid();
+    lookup[KEY_PEER] = remoteSelf;
+
+    Json::FastWriter writer;
+    std::string s = writer.write(lookup);
+
+    Json::Value answer;
+    if (peers.DoPost(answer, query.GetPeer(), URI_PULL, s) &&
+        answer.type() == Json::objectValue &&
+        answer.isMember(KEY_ID) &&
+        answer.isMember(KEY_PATH) &&
+        answer[KEY_ID].type() == Json::stringValue &&
+        answer[KEY_PATH].type() == Json::stringValue)
+    {
+      const std::string url = peers.GetPeerUrl(query.GetPeer());
+
+      Json::Value result = Json::objectValue;
+      result[KEY_PEER] = query.GetPeer();
+      result[KEY_REMOTE_JOB] = answer[KEY_ID].asString();
+      result[KEY_URL] = url + answer[KEY_PATH].asString();
+
+      std::string s = result.toStyledString();  
+      OrthancPluginAnswerBuffer(context.GetOrthanc(), output, s.c_str(), s.size(), "application/json");
+    }
+    else
+    {
+      LOG(ERROR) << "Cannot trigger send DICOM instances using pull mode to peer: " << query.GetPeer()
+                 << " (check out remote logs, and that transfer plugin is installed)";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }
+  }
+  else
+  {
+    SubmitJob(output, new OrthancPlugins::PushJob(context.GetOrthanc(), query,
+                                                  context.GetCache(),
+                                                  context.GetThreadsCount(),
+                                                  context.GetTargetBucketSize()),
+              query.GetPriority());
+  }
+}
+
+
+OrthancPluginJob* Unserializer(const char* jobType,
+                               const char* serialized)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  if (jobType == NULL ||
+      serialized == NULL)
+  {
+    return NULL;
+  }
+
+  std::string type(jobType);
+
+  if (type != JOB_TYPE_PULL &&
+      type != JOB_TYPE_PUSH)
+  {
+    return NULL;
+  }
+
+  try
+  {
+    std::string tmp(serialized);
+
+    Json::Value source;
+    Json::Reader reader;
+    if (reader.parse(tmp, source))
+    {
+      OrthancPlugins::TransferQuery query(source);
+
+      std::auto_ptr<OrthancPlugins::OrthancJob> job;
+
+      if (type == JOB_TYPE_PULL)
+      {
+        job.reset(new OrthancPlugins::PullJob(context.GetOrthanc(), query,
+                                              context.GetThreadsCount(),
+                                              context.GetTargetBucketSize()));
+      }
+      else if (type == JOB_TYPE_PUSH)
+      {
+        job.reset(new OrthancPlugins::PushJob(context.GetOrthanc(), query,
+                                              context.GetCache(),
+                                              context.GetThreadsCount(),
+                                              context.GetTargetBucketSize()));
+      }
+
+      if (job.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      else
+      {
+        return OrthancPlugins::OrthancJob::Create(context.GetOrthanc(), job.release());
+      }
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Error while unserializing a job from the transfers accelerator plugin: "
+               << e.What();
+    return NULL;
+  }
+  catch (...)
+  {
+    LOG(ERROR) << "Error while unserializing a job from the transfers accelerator plugin";
+    return NULL;
+  }
+}
+
+
+
+void ServePeers(OrthancPluginRestOutput* output,
+                const char* url,
+                const OrthancPluginHttpRequest* request)
+{
+  OrthancPlugins::PluginContext& context = OrthancPlugins::PluginContext::GetInstance();
+  
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context.GetOrthanc(), output, "GET");
+    return;
+  }
+
+  std::set<std::string> activePeers;
+  OrthancPlugins::DetectTransferPlugin::Apply
+    (activePeers, context.GetOrthanc(), context.GetThreadsCount(), 2 /* timeout */);
+
+  Json::Value result = Json::arrayValue;
+
+  for (std::set<std::string>::const_iterator
+         it = activePeers.begin(); it != activePeers.end(); ++it)
+  {
+    result.append(*it);
+  }
+
+  std::string s = result.toStyledString();
+  OrthancPluginAnswerBuffer(context.GetOrthanc(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    Orthanc::Logging::Initialize(context);
+    assert(DisplayPerformanceWarning());
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(context) == 0)
+    {
+      LOG(ERROR) << "Your version of Orthanc (" 
+                 << context->orthancVersion << ") must be above "
+                 << ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER << "."
+                 << ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER << "."
+                 << ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER
+                 << " to run this plugin";
+      return -1;
+    }
+
+    OrthancPluginSetDescription(context, "Accelerates transfers and provides "
+                                "storage commitment between Orthanc peers");
+
+    try
+    {
+      size_t threadsCount = 4;
+      size_t targetBucketSize = 4096;  // In KB
+      size_t maxPushTransactions = 4;
+      size_t memoryCacheSize = 512;    // In MB
+      std::map<std::string, std::string> bidirectionalPeers;
+    
+      {
+        OrthancPlugins::OrthancConfiguration config(context);
+
+        if (config.IsSection(KEY_PLUGIN_CONFIGURATION))
+        {
+          OrthancPlugins::OrthancConfiguration plugin;
+          config.GetSection(plugin, KEY_PLUGIN_CONFIGURATION);
+
+          plugin.GetDictionary(bidirectionalPeers, KEY_BIDIRECTIONAL_PEERS);
+          threadsCount = plugin.GetUnsignedIntegerValue("Threads", threadsCount);
+          targetBucketSize = plugin.GetUnsignedIntegerValue("BucketSize", targetBucketSize);
+          memoryCacheSize = plugin.GetUnsignedIntegerValue("CacheSize", memoryCacheSize);
+          maxPushTransactions = plugin.GetUnsignedIntegerValue
+            ("MaxPushTransactions", maxPushTransactions);
+        }
+      }
+
+      OrthancPlugins::PluginContext::Initialize
+        (context, threadsCount, targetBucketSize * KB, maxPushTransactions, memoryCacheSize * MB);
+      OrthancPlugins::PluginContext::GetInstance().LoadBidirectionalPeers(bidirectionalPeers);
+    
+      OrthancPlugins::RegisterRestCallback<ServeChunks>
+        (context, std::string(URI_CHUNKS) + "/([.0-9a-f-]+)", true);
+
+      OrthancPlugins::RegisterRestCallback<LookupInstances>
+        (context, URI_LOOKUP, true);
+
+      OrthancPlugins::RegisterRestCallback<SchedulePull>
+        (context, URI_PULL, true);
+
+      OrthancPlugins::RegisterRestCallback<ScheduleSend>
+        (context, URI_SEND, true);
+
+      OrthancPlugins::RegisterRestCallback<ServePeers>
+        (context, URI_PEERS, true);
+
+      if (maxPushTransactions != 0)
+      {
+        // If no push transaction is allowed, their URIs are disabled
+        OrthancPlugins::RegisterRestCallback<CreatePush>
+          (context, URI_PUSH, true);
+
+        OrthancPlugins::RegisterRestCallback<StorePush>
+          (context, std::string(URI_PUSH) + "/([.0-9a-f-]+)/([0-9]+)", true);
+
+        OrthancPlugins::RegisterRestCallback<CommitPush>
+          (context, std::string(URI_PUSH) + "/([.0-9a-f-]+)/commit", true);
+    
+        OrthancPlugins::RegisterRestCallback<DiscardPush>
+          (context, std::string(URI_PUSH) + "/([.0-9a-f-]+)", true);
+      }
+
+      OrthancPluginRegisterJobsUnserializer(context, Unserializer);
+
+      /* Extend the default Orthanc Explorer with custom JavaScript */
+      std::string explorer;
+      Orthanc::EmbeddedResources::GetFileResource
+        (explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
+      OrthancPluginExtendOrthancExplorer(context, explorer.c_str());
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << "Cannot initialize transfers accelerator plugin: " << e.What();
+      return -1;
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    LOG(WARNING) << "Transfers accelerator plugin is finalizing";
+
+    try
+    {
+      OrthancPlugins::PluginContext::Finalize();
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << "Error while finalizing the transfers accelerator plugin: " << e.What();
+    }
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return PLUGIN_NAME;
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/PluginContext.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,109 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PluginContext.h"
+
+#include <Core/Logging.h>
+
+
+namespace OrthancPlugins
+{
+  PluginContext::PluginContext(OrthancPluginContext* context,
+                               size_t threadsCount,
+                               size_t targetBucketSize,
+                               size_t maxPushTransactions,
+                               size_t memoryCacheSize) :
+    context_(context),
+    cache_(context),
+    pushTransactions_(maxPushTransactions),
+    semaphore_(threadsCount),
+    threadsCount_(threadsCount),
+    targetBucketSize_(targetBucketSize)
+  {
+    pluginUuid_ = Orthanc::Toolbox::GenerateUuid();
+
+    cache_.SetMaxMemorySize(memoryCacheSize);
+
+    LOG(INFO) << "Transfers accelerator will use " << threadsCount << " threads to run HTTP queries";
+    LOG(INFO) << "Transfers accelerator will use keep local DICOM files in a memory cache of size: "
+              << OrthancPlugins::ConvertToMegabytes(memoryCacheSize) << " MB";
+    LOG(INFO) << "Transfers accelerator will aim at HTTP queries of size: "
+              << OrthancPlugins::ConvertToKilobytes(targetBucketSize) << " KB";
+    LOG(INFO) << "Transfers accelerator will be able to receive up to "
+              << maxPushTransactions << " push transactions at once";
+
+  }
+
+
+  std::auto_ptr<PluginContext>& PluginContext::GetSingleton()
+  {
+    static std::auto_ptr<PluginContext>  singleton_;
+    return singleton_;
+  }
+
+  
+  bool PluginContext::LookupBidirectionalPeer(std::string& remoteSelf,
+                                              const std::string& remotePeer) const
+  {
+    BidirectionalPeers::const_iterator found = bidirectionalPeers_.find(remotePeer);
+
+    if (found == bidirectionalPeers_.end())
+    {
+      return false;
+    }
+    else
+    {
+      remoteSelf = found->second;
+      return true;
+    }
+  }
+  
+
+  void PluginContext::Initialize(OrthancPluginContext* context,
+                                 size_t threadsCount,
+                                 size_t targetBucketSize,
+                                 size_t maxPushTransactions,
+                                 size_t memoryCacheSize)
+  {
+    GetSingleton().reset(new PluginContext(context, threadsCount, targetBucketSize,
+                                           maxPushTransactions, memoryCacheSize));
+  }
+
+  
+  PluginContext& PluginContext::GetInstance()
+  {
+    if (GetSingleton().get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *GetSingleton();
+    }
+  }
+  
+
+  void PluginContext::Finalize()
+  {
+    if (GetSingleton().get() != NULL)
+    {
+      GetSingleton().reset();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/PluginContext.h	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,117 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Framework/OrthancInstancesCache.h"
+#include "../Framework/PushMode/ActivePushTransactions.h"
+
+#include <Core/MultiThreading/Semaphore.h>
+
+#include <map>
+
+namespace OrthancPlugins
+{
+  class PluginContext : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, std::string>  BidirectionalPeers;
+    
+    // Runtime structures
+    OrthancPluginContext*    context_;
+    OrthancInstancesCache    cache_;
+    ActivePushTransactions   pushTransactions_;
+    Orthanc::Semaphore       semaphore_;
+    std::string              pluginUuid_;
+
+    // Configuration
+    size_t                   threadsCount_;
+    size_t                   targetBucketSize_;
+    BidirectionalPeers       bidirectionalPeers_;
+
+  
+    PluginContext(OrthancPluginContext* context,
+                  size_t threadsCount,
+                  size_t targetBucketSize,
+                  size_t maxPushTransactions,
+                  size_t memoryCacheSize);
+
+    static std::auto_ptr<PluginContext>& GetSingleton();
+  
+  public:
+    OrthancPluginContext* GetOrthanc()
+    {
+      return context_;
+    }
+    
+    OrthancInstancesCache& GetCache()
+    {
+      return cache_;
+    }
+
+    ActivePushTransactions& GetActivePushTransactions()
+    {
+      return pushTransactions_;
+    }
+
+    Orthanc::Semaphore& GetSemaphore()
+    {
+      return semaphore_;
+    }
+
+    const std::string& GetPluginUuid() const
+    {
+      return pluginUuid_;
+    }
+
+    size_t GetThreadsCount() const
+    {
+      return threadsCount_;
+    }
+
+    size_t GetTargetBucketSize() const
+    {
+      return targetBucketSize_;
+    }
+
+    void AddBidirectionalPeer(const std::string& remotePeer,
+                              const std::string& remoteSelf)
+    {
+      bidirectionalPeers_[remotePeer] = remoteSelf;
+    }
+
+    void LoadBidirectionalPeers(const BidirectionalPeers& peers)
+    {
+      bidirectionalPeers_ = peers;
+    }
+
+    bool LookupBidirectionalPeer(std::string& remoteSelf,
+                                 const std::string& remotePeer) const;
+  
+    static void Initialize(OrthancPluginContext* context,
+                           size_t threadsCount,
+                           size_t targetBucketSize,
+                           size_t maxPushTransactions,
+                           size_t memoryCacheSize);
+  
+    static PluginContext& GetInstance();
+
+    static void Finalize();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,86 @@
+Transfers accelerator plugin for Orthanc
+========================================
+
+
+General information
+-------------------
+
+This repository contains the source code of a plugin for Orthanc that
+speeds up the transfers of DICOM instances over Internet.
+
+The plugin can be used to send local images to remote Orthanc peers,
+or to retrieve images stored on remote Orthanc peers.
+
+Technically, this plugin extends the REST API of Orthanc with
+endpoints that optimize the use of the network bandwidth over the HTTP
+and HTTPS protocols, through the combination of the following
+mechanisms:
+
+* Small DICOM instances are grouped together to form so-called
+  "buckets" of some megabytes in order to reduce the number of HTTP
+  handshakes.
+
+* Large DICOM instances are split as a set of smaller buckets in order
+  to bypass nasty effects of TCP congestion control on low-quality
+  network links.
+
+* Buckets are download concurrently by several threads.
+
+* Buckets can be individually compressed using the gzip algorithm,
+  hereby reducing the network usage. On a typical medical image, this
+  can divide the volume of the transmission by a factor 2 to 3, at the
+  price of a larger CPU usage.
+
+* Sending images to remote Orthanc peers can either be done with HTTP
+  PUT requests (so-called "push mode"), or with HTTP GET requests if
+  the local Orthanc server has a public IP address (so-called "pull
+  mode").
+
+Note that the protocol is built over HTTP/HTTPS (and not directly over
+TCP), making it friendly with network firewalls and Web caches. Also,
+the plugin takes advantage of the jobs engine of Orthanc, so that
+transfers can be easily paused/canceled/resubmitted.
+
+
+Content
+-------
+
+* ./Framework/         - Core C++ framework
+* ./Plugin/            - Source code of the plugin
+* ./Resources/         - 
+* ./UnitTestsSources/  - Unit tests
+
+
+Compilation and usage
+---------------------
+
+The compilation and usage of the plugin is part of the Orthanc Book:
+http://book.orthanc-server.com/plugins/transfers.html
+
+
+Licensing
+---------
+
+The transfers accelerator plugin for Orthanc is licensed under the
+AGPL license.
+
+We also kindly ask scientific works and clinical studies that make
+use of Orthanc to cite Orthanc in their associated publications.
+Similarly, we ask open-source and closed-source products that make
+use of Orthanc to warn us about this use. You can cite our work
+using the following BibTeX entry:
+
+@Article{Jodogne2018,
+  author="Jodogne, S{\'e}bastien",
+  title="The {O}rthanc Ecosystem for Medical Imaging",
+  journal="Journal of Digital Imaging",
+  year="2018",
+  month="Jun",
+  day="01",
+  volume="31",
+  number="3",
+  pages="341--352",
+  issn="1618-727X",
+  doi="10.1007/s10278-018-0082-y",
+  url="https://doi.org/10.1007/s10278-018-0082-y"
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Orthanc/DownloadOrthancFramework.cmake	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,325 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2018 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+
+##
+## Check whether the parent script sets the mandatory variables
+##
+
+if (NOT DEFINED ORTHANC_FRAMEWORK_SOURCE OR
+    (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" AND
+     NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "web" AND
+     NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" AND
+     NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "path"))
+  message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_SOURCE must be set to \"hg\", \"web\", \"archive\" or \"path\"")
+endif()
+
+
+##
+## Detection of the requested version
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+  if (NOT DEFINED ORTHANC_FRAMEWORK_VERSION)
+    message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_VERSION must be set")
+  endif()
+
+  if (DEFINED ORTHANC_FRAMEWORK_MAJOR OR
+      DEFINED ORTHANC_FRAMEWORK_MINOR OR
+      DEFINED ORTHANC_FRAMEWORK_REVISION OR
+      DEFINED ORTHANC_FRAMEWORK_MD5)
+    message(FATAL_ERROR "Some internal variable has been set")
+  endif()
+
+  set(ORTHANC_FRAMEWORK_MD5 "")
+
+  if (NOT DEFINED ORTHANC_FRAMEWORK_BRANCH)
+    if (ORTHANC_FRAMEWORK_VERSION STREQUAL "mainline")
+      set(ORTHANC_FRAMEWORK_BRANCH "default")
+
+    else()
+      set(ORTHANC_FRAMEWORK_BRANCH "Orthanc-${ORTHANC_FRAMEWORK_VERSION}")
+
+      set(RE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
+      string(REGEX REPLACE ${RE} "\\1" ORTHANC_FRAMEWORK_MAJOR ${ORTHANC_FRAMEWORK_VERSION})
+      string(REGEX REPLACE ${RE} "\\2" ORTHANC_FRAMEWORK_MINOR ${ORTHANC_FRAMEWORK_VERSION})
+      string(REGEX REPLACE ${RE} "\\3" ORTHANC_FRAMEWORK_REVISION ${ORTHANC_FRAMEWORK_VERSION})
+
+      if (NOT ORTHANC_FRAMEWORK_MAJOR MATCHES "^[0-9]+$" OR
+          NOT ORTHANC_FRAMEWORK_MINOR MATCHES "^[0-9]+$" OR
+          NOT ORTHANC_FRAMEWORK_REVISION MATCHES "^[0-9]+$")
+        message("Bad version of the Orthanc framework: ${ORTHANC_FRAMEWORK_VERSION}")
+      endif()
+
+      if (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.1")
+        set(ORTHANC_FRAMEWORK_MD5 "dac95bd6cf86fb19deaf4e612961f378")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.2")
+        set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0")
+        set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a")
+      endif()
+    endif()
+  endif()
+endif()
+
+
+
+##
+## Detection of the third-party software
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg")
+  find_program(ORTHANC_FRAMEWORK_HG hg)
+  
+  if (${ORTHANC_FRAMEWORK_HG} MATCHES "ORTHANC_FRAMEWORK_HG-NOTFOUND")
+    message(FATAL_ERROR "Please install Mercurial")
+  endif()
+endif()
+
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+  if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+    find_program(ORTHANC_FRAMEWORK_7ZIP 7z 
+      PATHS 
+      "$ENV{ProgramFiles}/7-Zip"
+      "$ENV{ProgramW6432}/7-Zip"
+      )
+
+    if (${ORTHANC_FRAMEWORK_7ZIP} MATCHES "ORTHANC_FRAMEWORK_7ZIP-NOTFOUND")
+      message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)")
+    endif()
+
+  else()
+    find_program(ORTHANC_FRAMEWORK_TAR tar)
+    if (${ORTHANC_FRAMEWORK_TAR} MATCHES "ORTHANC_FRAMEWORK_TAR-NOTFOUND")
+      message(FATAL_ERROR "Please install the 'tar' package")
+    endif()
+  endif()
+endif()
+
+
+
+##
+## Case of the Orthanc framework specified as a path on the filesystem
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path")
+  if (NOT DEFINED ORTHANC_FRAMEWORK_ROOT)
+    message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ROOT must provide the path to the sources of Orthanc")
+  endif()
+  
+  if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT})
+    message(FATAL_ERROR "Non-existing directory: ${ORTHANC_FRAMEWORK_ROOT}")
+  endif()
+  
+  if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake)
+    message(FATAL_ERROR "Directory not containing the source code of Orthanc: ${ORTHANC_FRAMEWORK_ROOT}")
+  endif()
+  
+  set(ORTHANC_ROOT ${ORTHANC_FRAMEWORK_ROOT})
+endif()
+
+
+
+##
+## Case of the Orthanc framework cloned using Mercurial
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg")
+  if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
+    message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
+  endif()
+
+  set(ORTHANC_ROOT ${CMAKE_BINARY_DIR}/orthanc)
+
+  if (EXISTS ${ORTHANC_ROOT})
+    message("Updating the Orthanc source repository using Mercurial")
+    execute_process(
+      COMMAND ${ORTHANC_FRAMEWORK_HG} pull
+      WORKING_DIRECTORY ${ORTHANC_ROOT}
+      RESULT_VARIABLE Failure
+      )    
+  else()
+    message("Forking the Orthanc source repository using Mercurial")
+    execute_process(
+      COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://bitbucket.org/sjodogne/orthanc"
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )    
+  endif()
+
+  if (Failure OR NOT EXISTS ${ORTHANC_ROOT})
+    message(FATAL_ERROR "Cannot fork the Orthanc repository")
+  endif()
+
+  message("Setting branch of the Orthanc repository to: ${ORTHANC_FRAMEWORK_BRANCH}")
+
+  execute_process(
+    COMMAND ${ORTHANC_FRAMEWORK_HG} update -c ${ORTHANC_FRAMEWORK_BRANCH}
+    WORKING_DIRECTORY ${ORTHANC_ROOT}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while running Mercurial")
+  endif()
+endif()
+
+
+
+##
+## Case of the Orthanc framework provided as a source archive on the
+## filesystem
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive")
+  if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE)
+    message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ARCHIVE must provide the path to the sources of Orthanc")
+  endif()
+endif()
+
+
+
+##
+## Case of the Orthanc framework downloaded from the Web
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+  if (DEFINED ORTHANC_FRAMEWORK_URL)
+    string(REGEX REPLACE "^.*/" "" ORTHANC_FRAMEMORK_FILENAME "${ORTHANC_FRAMEWORK_URL}")
+  else()
+    # Default case: Download from the official Web site
+    set(ORTHANC_FRAMEMORK_FILENAME Orthanc-${ORTHANC_FRAMEWORK_VERSION}.tar.gz)
+    #set(ORTHANC_FRAMEWORK_URL "http://www.orthanc-server.com/downloads/get.php?path=/orthanc/${ORTHANC_FRAMEMORK_FILENAME}")
+    set(ORTHANC_FRAMEWORK_URL "http://www.orthanc-server.com/downloads/third-party/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}")
+  endif()
+
+  set(ORTHANC_FRAMEWORK_ARCHIVE "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${ORTHANC_FRAMEMORK_FILENAME}")
+
+  if (NOT EXISTS "${ORTHANC_FRAMEWORK_ARCHIVE}")
+    if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
+      message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
+    endif()
+
+    message("Downloading: ${ORTHANC_FRAMEWORK_URL}")
+
+    file(DOWNLOAD
+      "${ORTHANC_FRAMEWORK_URL}" "${ORTHANC_FRAMEWORK_ARCHIVE}" 
+      SHOW_PROGRESS EXPECTED_MD5 "${ORTHANC_FRAMEWORK_MD5}"
+      TIMEOUT 60
+      INACTIVITY_TIMEOUT 60
+      )
+  else()
+    message("Using local copy of: ${ORTHANC_FRAMEWORK_URL}")
+  endif()  
+endif()
+
+
+
+
+##
+## Uncompressing the Orthanc framework, if it was retrieved from a
+## source archive on the filesystem, or from the official Web site
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+
+  if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE OR
+      NOT DEFINED ORTHANC_FRAMEWORK_VERSION OR
+      NOT DEFINED ORTHANC_FRAMEWORK_MD5)
+    message(FATAL_ERROR "Internal error")
+  endif()
+
+  if (ORTHANC_FRAMEWORK_MD5 STREQUAL "")
+    message(FATAL_ERROR "Unknown release of Orthanc: ${ORTHANC_FRAMEWORK_VERSION}")
+  endif()
+
+  file(MD5 ${ORTHANC_FRAMEWORK_ARCHIVE} ActualMD5)
+
+  if (NOT "${ActualMD5}" STREQUAL "${ORTHANC_FRAMEWORK_MD5}")
+    message(FATAL_ERROR "The MD5 hash of the Orthanc archive is invalid: ${ORTHANC_FRAMEWORK_ARCHIVE}")
+  endif()
+
+  set(ORTHANC_ROOT "${CMAKE_BINARY_DIR}/Orthanc-${ORTHANC_FRAMEWORK_VERSION}")
+
+  if (NOT IS_DIRECTORY "${ORTHANC_ROOT}")
+    if (NOT ORTHANC_FRAMEWORK_ARCHIVE MATCHES ".tar.gz$")
+      message(FATAL_ERROR "Archive should have the \".tar.gz\" extension: ${ORTHANC_FRAMEWORK_ARCHIVE}")
+    endif()
+    
+    message("Uncompressing: ${ORTHANC_FRAMEWORK_ARCHIVE}")
+
+    if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+      # How to silently extract files using 7-zip
+      # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
+
+      execute_process(
+        COMMAND ${ORTHANC_FRAMEWORK_7ZIP} e -y ${ORTHANC_FRAMEWORK_ARCHIVE}
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        OUTPUT_QUIET
+        )
+      
+      if (Failure)
+        message(FATAL_ERROR "Error while running the uncompression tool")
+      endif()
+
+      get_filename_component(TMP_FILENAME "${ORTHANC_FRAMEWORK_ARCHIVE}" NAME)
+      string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}")
+
+      execute_process(
+        COMMAND ${ORTHANC_FRAMEWORK_7ZIP} x -y ${TMP_FILENAME2}
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        OUTPUT_QUIET
+        )
+
+    else()
+      execute_process(
+        COMMAND sh -c "${ORTHANC_FRAMEWORK_TAR} xfz ${ORTHANC_FRAMEWORK_ARCHIVE}"
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        )
+    endif()
+   
+    if (Failure)
+      message(FATAL_ERROR "Error while running the uncompression tool")
+    endif()
+
+    if (NOT IS_DIRECTORY "${ORTHANC_ROOT}")
+      message(FATAL_ERROR "The Orthanc framework was not uncompressed at the proper location. Check the CMake instructions.")
+    endif()
+  endif()
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Orthanc/LinuxStandardBaseToolchain.cmake	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,66 @@
+# LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON
+
+INCLUDE(CMakeForceCompiler)
+
+SET(LSB_PATH $ENV{LSB_PATH})
+SET(LSB_CC $ENV{LSB_CC})
+SET(LSB_CXX $ENV{LSB_CXX})
+SET(LSB_TARGET_VERSION "4.0")
+
+IF ("${LSB_PATH}" STREQUAL "")
+  SET(LSB_PATH "/opt/lsb")
+ENDIF()
+
+IF (EXISTS ${LSB_PATH}/lib64)
+  SET(LSB_TARGET_PROCESSOR "x86_64")
+  SET(LSB_LIBPATH ${LSB_PATH}/lib64-${LSB_TARGET_VERSION})
+ELSEIF (EXISTS ${LSB_PATH}/lib)
+  SET(LSB_TARGET_PROCESSOR "x86")
+  SET(LSB_LIBPATH ${LSB_PATH}/lib-${LSB_TARGET_VERSION})
+ELSE()
+  MESSAGE(FATAL_ERROR "Unable to detect the target processor architecture. Check the LSB_PATH environment variable.")
+ENDIF()
+
+SET(LSB_CPPPATH ${LSB_PATH}/include)
+SET(PKG_CONFIG_PATH ${LSB_LIBPATH}/pkgconfig/)
+
+# the name of the target operating system
+SET(CMAKE_SYSTEM_NAME Linux)
+SET(CMAKE_SYSTEM_VERSION LinuxStandardBase)
+SET(CMAKE_SYSTEM_PROCESSOR ${LSB_TARGET_PROCESSOR})
+
+# which compilers to use for C and C++
+SET(CMAKE_C_COMPILER ${LSB_PATH}/bin/lsbcc)
+CMAKE_FORCE_CXX_COMPILER(${LSB_PATH}/bin/lsbc++ GNU)
+
+# here is the target environment located
+SET(CMAKE_FIND_ROOT_PATH ${LSB_PATH})
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
+SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
+
+SET(CMAKE_CROSSCOMPILING OFF)
+
+
+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include" CACHE INTERNAL "" FORCE)
+SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward" CACHE INTERNAL "" FORCE)
+SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE)
+SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE)
+
+if (NOT "${LSB_CXX}" STREQUAL "")
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cxx=${LSB_CXX}")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}")
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}")
+endif()
+
+if (NOT "${LSB_CC}" STREQUAL "")
+  SET(CMAKE_C_FLAGS "${CMAKE_CC_FLAGS} --lsb-cc=${LSB_CC}")
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cc=${LSB_CC}")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cc=${LSB_CC}")
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cc=${LSB_CC}")
+endif()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Orthanc/MinGW-W64-Toolchain32.cmake	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,17 @@
+# the name of the target operating system
+set(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+set(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
+set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
+set(CMAKE_RC_COMPILER i686-w64-mingw32-windres)
+
+# here is the target environment located
+set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Orthanc/MinGW-W64-Toolchain64.cmake	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,17 @@
+# the name of the target operating system
+set(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
+set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
+set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
+
+# here is the target environment located
+set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Orthanc/MinGWToolchain.cmake	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,20 @@
+# the name of the target operating system
+set(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+set(CMAKE_C_COMPILER i586-mingw32msvc-gcc)
+set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)
+set(CMAKE_RC_COMPILER i586-mingw32msvc-windres)
+
+# here is the target environment located
+set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE)
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/OrthancExplorer.js	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,111 @@
+function TransferAcceleratorSelectPeer(callback)
+{
+  var items = $('<ul>')
+    .attr('data-divider-theme', 'd')
+    .attr('data-role', 'listview');
+
+  items.append('<li data-role="list-divider">Orthanc peers</li>');
+
+  $.ajax({
+    url: '../transfers/peers',
+    type: 'GET',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(peers) {
+      for (var i = 0; i < peers.length; i++) {
+        var name = peers[i];
+        var item = $('<li>')
+          .html('<a href="#" rel="close">' + name + '</a>')
+          .attr('name', name)
+          .click(function() { 
+            clickedPeer = $(this).attr('name');
+          });
+        items.append(item);
+      }
+
+      // Launch the dialog
+      $('#dialog').simpledialog2({
+        mode: 'blank',
+        animate: false,
+        headerText: 'Choose target',
+        headerClose: true,
+        forceInput: false,
+        width: '100%',
+        blankContent: items,
+        callbackClose: function() {
+          var timer;
+          function WaitForDialogToClose() {
+            if (!$('#dialog').is(':visible')) {
+              clearInterval(timer);
+              callback(clickedPeer);
+            }
+          }
+          timer = setInterval(WaitForDialogToClose, 100);
+        }
+      });
+    }
+  });
+}
+
+
+function TransferAcceleratorAddSendButton(level, siblingButton)
+{
+  var b = $('<a>')
+    .attr('data-role', 'button')
+    .attr('href', '#')
+    .attr('data-icon', 'search')
+    .attr('data-theme', 'e')
+    .text('Transfer accelerator');
+
+  b.insertBefore($(siblingButton).parent().parent());
+
+  b.click(function() {
+    if ($.mobile.pageData) {
+      var uuid = $.mobile.pageData.uuid;
+      TransferAcceleratorSelectPeer(function(peer) {
+        console.log('Sending ' + level + ' ' + uuid + ' to peer ' + peer);
+
+        var query = {
+          'Resources' : [
+            {
+              'Level' : level,
+              'ID' : uuid
+            }
+          ], 
+          'Compression' : 'gzip',
+          'Peer' : peer
+        };
+
+        $.ajax({
+          url: '../transfers/send',
+          type: 'POST',
+          dataType: 'json',
+          data: JSON.stringify(query),
+          success: function(job) {
+            if (!(typeof job.ID === 'undefined')) {
+              $.mobile.changePage('#job?uuid=' + job.ID);
+            }
+          },
+          error: function() {
+            alert('Error while creating the transfer job');
+          }
+        });  
+      });
+    }
+  });
+}
+
+
+
+$('#patient').live('pagebeforecreate', function() {
+  TransferAcceleratorAddSendButton('Patient', '#patient-delete');
+});
+
+$('#study').live('pagebeforecreate', function() {
+  TransferAcceleratorAddSendButton('Study', '#study-delete');
+});
+
+$('#series').live('pagebeforecreate', function() {
+  TransferAcceleratorAddSendButton('Series', '#series-delete');
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/SyncOrthancFolder.py	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,64 @@
+#!/usr/bin/python
+
+#
+# This maintenance script updates the content of the "Orthanc" folder
+# to match the latest version of the Orthanc source code.
+#
+
+import multiprocessing
+import os
+import stat
+import urllib2
+
+TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc')
+PLUGIN_SDK_VERSION = [ '1.4.2' ]
+REPOSITORY = 'https://bitbucket.org/sjodogne/orthanc/raw'
+
+FILES = [
+    'DownloadOrthancFramework.cmake',
+    'LinuxStandardBaseToolchain.cmake',
+    'MinGW-W64-Toolchain32.cmake',
+    'MinGW-W64-Toolchain64.cmake',
+    'MinGWToolchain.cmake',
+]
+
+SDK = [
+    'orthanc/OrthancCPlugin.h',
+]
+
+
+def Download(x):
+    branch = x[0]
+    source = x[1]
+    target = os.path.join(TARGET, x[2])
+    print target
+
+    try:
+        os.makedirs(os.path.dirname(target))
+    except:
+        pass
+
+    url = '%s/%s/%s' % (REPOSITORY, branch, source)
+
+    with open(target, 'w') as f:
+        f.write(urllib2.urlopen(url).read())
+
+
+commands = []
+
+for f in FILES:
+    commands.append([ 'default',
+                      os.path.join('Resources', f),
+                      f ])
+
+#for version in PLUGIN_SDK_VERSION:
+#    for f in SDK:
+#        commands.append([
+#            'Orthanc-%s' % version, 
+#            'Plugins/Include/%s' % f,
+#            'Sdk-%s/%s' % (version, f) 
+#        ])
+
+
+pool = multiprocessing.Pool(10)  # simultaneous downloads
+pool.map(Download, commands)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTests/UnitTestsMain.cpp	Mon Sep 17 11:34:55 2018 +0200
@@ -0,0 +1,445 @@
+/**
+ * Transfers accelerator plugin for Orthanc
+ * Copyright (C) 2018 Osimis, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Framework/DownloadArea.h"
+
+#include <Core/Compression/GzipCompressor.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <gtest/gtest.h>
+
+
+TEST(Toolbox, Enumerations)
+{
+  using namespace OrthancPlugins;
+  ASSERT_EQ(BucketCompression_None, StringToBucketCompression(EnumerationToString(BucketCompression_None)));
+  ASSERT_EQ(BucketCompression_Gzip, StringToBucketCompression(EnumerationToString(BucketCompression_Gzip)));
+  ASSERT_THROW(StringToBucketCompression("None"), Orthanc::OrthancException);
+}
+
+
+TEST(Toolbox, Conversions)
+{
+  ASSERT_EQ(2u, OrthancPlugins::ConvertToKilobytes(2048));
+  ASSERT_EQ(1u, OrthancPlugins::ConvertToKilobytes(1000));
+  ASSERT_EQ(0u, OrthancPlugins::ConvertToKilobytes(500));
+
+  ASSERT_EQ(2u, OrthancPlugins::ConvertToMegabytes(2048 * 1024));
+  ASSERT_EQ(1u, OrthancPlugins::ConvertToMegabytes(1000 * 1024));
+  ASSERT_EQ(0u, OrthancPlugins::ConvertToMegabytes(500 * 1024));
+}
+
+
+TEST(TransferBucket, Basic)
+{  
+  using namespace OrthancPlugins;
+
+  DicomInstanceInfo d1("d1", 10, "");
+  DicomInstanceInfo d2("d2", 20, "");
+  DicomInstanceInfo d3("d3", 30, "");
+  DicomInstanceInfo d4("d4", 40, "");
+
+  {
+    TransferBucket b;
+    ASSERT_EQ(0u, b.GetTotalSize());
+    ASSERT_EQ(0u, b.GetChunksCount());
+
+    b.AddChunk(d1, 0, 10);
+    b.AddChunk(d2, 0, 20);
+    ASSERT_THROW(b.AddChunk(d3, 0, 31), Orthanc::OrthancException);
+    ASSERT_THROW(b.AddChunk(d3, 1, 30), Orthanc::OrthancException);
+    b.AddChunk(d3, 0, 30);
+    
+    ASSERT_EQ(60u, b.GetTotalSize());
+    ASSERT_EQ(3u, b.GetChunksCount());
+
+    ASSERT_EQ("d1", b.GetChunkInstanceId(0));
+    ASSERT_EQ(0u, b.GetChunkOffset(0));
+    ASSERT_EQ(10u, b.GetChunkSize(0));
+    ASSERT_EQ("d2", b.GetChunkInstanceId(1));
+    ASSERT_EQ(0u, b.GetChunkOffset(1));
+    ASSERT_EQ(20u, b.GetChunkSize(1));
+    ASSERT_EQ("d3", b.GetChunkInstanceId(2));
+    ASSERT_EQ(0u, b.GetChunkOffset(2));
+    ASSERT_EQ(30u, b.GetChunkSize(2));
+
+    std::string uri;
+    b.ComputePullUri(uri, BucketCompression_None);
+    ASSERT_EQ("/transfers/chunks/d1.d2.d3?offset=0&size=60&compression=none", uri);
+    b.ComputePullUri(uri, BucketCompression_Gzip);
+    ASSERT_EQ("/transfers/chunks/d1.d2.d3?offset=0&size=60&compression=gzip", uri);
+      
+    b.Clear();
+    ASSERT_EQ(0u, b.GetTotalSize());
+    ASSERT_EQ(0u, b.GetChunksCount());
+
+    ASSERT_THROW(b.ComputePullUri(uri, BucketCompression_None), Orthanc::OrthancException);  // Empty
+  }
+
+  {
+    TransferBucket b;
+    b.AddChunk(d1, 5, 5);
+    ASSERT_THROW(b.AddChunk(d2, 1, 7), Orthanc::OrthancException);  // Can only skip bytes in 1st chunk
+    b.AddChunk(d2, 0, 20);
+    b.AddChunk(d3, 0, 7);
+    ASSERT_THROW(b.AddChunk(d4, 0, 10), Orthanc::OrthancException); // d2 was not complete
+
+    ASSERT_EQ(32u, b.GetTotalSize());
+    ASSERT_EQ(3u, b.GetChunksCount());
+
+    ASSERT_EQ("d1", b.GetChunkInstanceId(0));
+    ASSERT_EQ(5u, b.GetChunkOffset(0));
+    ASSERT_EQ(5u, b.GetChunkSize(0));
+    ASSERT_EQ("d2", b.GetChunkInstanceId(1));
+    ASSERT_EQ(0u, b.GetChunkOffset(1));
+    ASSERT_EQ(20u, b.GetChunkSize(1));
+    ASSERT_EQ("d3", b.GetChunkInstanceId(2));
+    ASSERT_EQ(0u, b.GetChunkOffset(2));
+    ASSERT_EQ(7u, b.GetChunkSize(2));
+    
+    std::string uri;
+    b.ComputePullUri(uri, BucketCompression_None);
+    ASSERT_EQ("/transfers/chunks/d1.d2.d3?offset=5&size=32&compression=none", uri);
+    b.ComputePullUri(uri, BucketCompression_Gzip);
+    ASSERT_EQ("/transfers/chunks/d1.d2.d3?offset=5&size=32&compression=gzip", uri);
+
+    b.Clear();
+    ASSERT_EQ(0u, b.GetTotalSize());
+    ASSERT_EQ(0u, b.GetChunksCount());
+
+    b.AddChunk(d2, 1, 7);
+    ASSERT_EQ(7u, b.GetTotalSize());
+    ASSERT_EQ(1u, b.GetChunksCount());
+  }
+}
+
+
+TEST(TransferBucket, Serialization)
+{  
+  using namespace OrthancPlugins;
+
+  Json::Value s;
+  
+  {
+    DicomInstanceInfo d1("d1", 10, "");
+    DicomInstanceInfo d2("d2", 20, "");
+    DicomInstanceInfo d3("d3", 30, "");
+
+    TransferBucket b;
+    b.AddChunk(d1, 5, 5);
+    b.AddChunk(d2, 0, 20);
+    b.AddChunk(d3, 0, 7);
+    b.Serialize(s);
+  }
+
+  {
+    TransferBucket b(s);
+
+    std::string uri;
+    b.ComputePullUri(uri, BucketCompression_None);
+    ASSERT_EQ("/transfers/chunks/d1.d2.d3?offset=5&size=32&compression=none", uri);
+  }
+}
+
+
+TEST(TransferScheduler, Empty)
+{  
+  using namespace OrthancPlugins;
+
+  TransferScheduler s;
+  ASSERT_EQ(0u, s.GetInstancesCount());
+  ASSERT_EQ(0u, s.GetTotalSize());
+
+  std::vector<DicomInstanceInfo> i;
+  s.ListInstances(i);
+  ASSERT_TRUE(i.empty());
+
+  std::vector<TransferBucket> b;
+  s.ComputePullBuckets(b, 10, 1000, "http://localhost/", BucketCompression_None);
+  ASSERT_TRUE(b.empty());
+
+  Json::Value v;
+  s.FormatPushTransaction(v, b, 10, 1000, BucketCompression_None);
+  ASSERT_TRUE(b.empty());
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_TRUE(v.isMember("Buckets"));
+  ASSERT_TRUE(v.isMember("Compression"));
+  ASSERT_TRUE(v.isMember("Instances"));
+  ASSERT_EQ(Json::arrayValue, v["Buckets"].type());
+  ASSERT_EQ(Json::stringValue, v["Compression"].type());
+  ASSERT_EQ(Json::arrayValue, v["Instances"].type());
+  ASSERT_EQ(0u, v["Buckets"].size());
+  ASSERT_EQ("none", v["Compression"].asString());
+  ASSERT_EQ(0u, v["Instances"].size());
+}
+
+
+TEST(TransferScheduler, Basic)
+{  
+  using namespace OrthancPlugins;
+
+  DicomInstanceInfo d1("d1", 10, "md1");
+  DicomInstanceInfo d2("d2", 10, "md2");
+  DicomInstanceInfo d3("d3", 10, "md3");
+
+  TransferScheduler s;
+  s.AddInstance(d1);
+  s.AddInstance(d2);
+  s.AddInstance(d3);
+
+  std::vector<DicomInstanceInfo> i;
+  s.ListInstances(i);
+  ASSERT_EQ(3u, i.size());
+
+  std::vector<TransferBucket> b;
+  s.ComputePullBuckets(b, 10, 1000, "http://localhost/", BucketCompression_None);
+  ASSERT_EQ(3u, b.size());
+  ASSERT_EQ(1u, b[0].GetChunksCount());
+  ASSERT_EQ("d1", b[0].GetChunkInstanceId(0));
+  ASSERT_EQ(0u, b[0].GetChunkOffset(0));
+  ASSERT_EQ(10u, b[0].GetChunkSize(0));
+  ASSERT_EQ(1u, b[1].GetChunksCount());
+  ASSERT_EQ("d2", b[1].GetChunkInstanceId(0));
+  ASSERT_EQ(0u, b[1].GetChunkOffset(0));
+  ASSERT_EQ(10u, b[1].GetChunkSize(0));
+  ASSERT_EQ(1u, b[2].GetChunksCount());
+  ASSERT_EQ("d3", b[2].GetChunkInstanceId(0));
+  ASSERT_EQ(0u, b[2].GetChunkOffset(0));
+  ASSERT_EQ(10u, b[2].GetChunkSize(0));
+
+  Json::Value v;
+  s.FormatPushTransaction(v, b, 10, 1000, BucketCompression_Gzip);
+  ASSERT_EQ(3u, b.size());
+  ASSERT_EQ(3u, v["Buckets"].size());
+  ASSERT_EQ("gzip", v["Compression"].asString());
+  ASSERT_EQ(3u, v["Instances"].size());
+
+  for (Json::Value::ArrayIndex i = 0; i < 3; i++)
+  {
+    TransferBucket b(v["Buckets"][i]);
+    ASSERT_EQ(1u, b.GetChunksCount());
+    if (i == 0)
+      ASSERT_EQ("d1", b.GetChunkInstanceId(0));
+    else if (i == 1)
+      ASSERT_EQ("d2", b.GetChunkInstanceId(0));
+    else
+      ASSERT_EQ("d3", b.GetChunkInstanceId(0));
+        
+    ASSERT_EQ(0u, b.GetChunkOffset(0));
+    ASSERT_EQ(10u, b.GetChunkSize(0));
+  }
+    
+  for (Json::Value::ArrayIndex i = 0; i < 3; i++)
+  {
+    DicomInstanceInfo d(v["Instances"][i]);
+    if (i == 0)
+    {
+      ASSERT_EQ("d1", d.GetId());
+      ASSERT_EQ("md1", d.GetMD5());
+    }
+    else if (i == 1)
+    {
+      ASSERT_EQ("d2", d.GetId());
+      ASSERT_EQ("md2", d.GetMD5());
+    }
+    else
+    {
+      ASSERT_EQ("d3", d.GetId());
+      ASSERT_EQ("md3", d.GetMD5());
+    }
+        
+    ASSERT_EQ(10u, d.GetSize());
+  }
+}
+
+
+
+TEST(TransferScheduler, Grouping)
+{  
+  using namespace OrthancPlugins;
+
+  DicomInstanceInfo d1("d1", 10, "md1");
+  DicomInstanceInfo d2("d2", 10, "md2");
+  DicomInstanceInfo d3("d3", 10, "md3");
+
+  TransferScheduler s;
+  s.AddInstance(d1);
+  s.AddInstance(d2);
+  s.AddInstance(d3);
+
+  {
+    std::vector<TransferBucket> b;
+    s.ComputePullBuckets(b, 20, 1000, "http://localhost/", BucketCompression_None);
+    ASSERT_EQ(2u, b.size());
+    ASSERT_EQ(2u, b[0].GetChunksCount());
+    ASSERT_EQ("d1", b[0].GetChunkInstanceId(0));
+    ASSERT_EQ("d2", b[0].GetChunkInstanceId(1));
+    ASSERT_EQ(1u, b[1].GetChunksCount());    
+    ASSERT_EQ("d3", b[1].GetChunkInstanceId(0));
+  }
+
+  {
+    std::vector<TransferBucket> b;
+    s.ComputePullBuckets(b, 21, 1000, "http://localhost/", BucketCompression_None);
+    ASSERT_EQ(1u, b.size());
+    ASSERT_EQ(3u, b[0].GetChunksCount());
+    ASSERT_EQ("d1", b[0].GetChunkInstanceId(0));
+    ASSERT_EQ("d2", b[0].GetChunkInstanceId(1));
+    ASSERT_EQ("d3", b[0].GetChunkInstanceId(2));
+  }
+
+  {
+    std::string longBase(2048, '_');
+    std::vector<TransferBucket> b;
+    s.ComputePullBuckets(b, 21, 1000, longBase, BucketCompression_None);
+    ASSERT_EQ(3u, b.size());
+    ASSERT_EQ(1u, b[0].GetChunksCount());
+    ASSERT_EQ("d1", b[0].GetChunkInstanceId(0));
+    ASSERT_EQ(1u, b[1].GetChunksCount());
+    ASSERT_EQ("d2", b[1].GetChunkInstanceId(0));
+    ASSERT_EQ(1u, b[2].GetChunksCount());
+    ASSERT_EQ("d3", b[2].GetChunkInstanceId(0));
+  }
+}
+
+
+TEST(TransferScheduler, Splitting)
+{  
+  using namespace OrthancPlugins;
+
+  for (size_t i = 1; i < 20; i++)
+  {
+    DicomInstanceInfo dicom("dicom", i, "");
+
+    TransferScheduler s;
+    s.AddInstance(dicom);
+
+    {
+      std::vector<TransferBucket> b;
+      s.ComputePullBuckets(b, 1, 1000, "http://localhost/", BucketCompression_None);
+      ASSERT_EQ(1u, b.size());
+      ASSERT_EQ(1u, b[0].GetChunksCount());
+      ASSERT_EQ("dicom", b[0].GetChunkInstanceId(0));
+      ASSERT_EQ(0u, b[0].GetChunkOffset(0));
+      ASSERT_EQ(i, b[0].GetChunkSize(0));
+    }
+
+    for (size_t split = 1; split < 20; split++)
+    {
+      size_t count;
+      if (dicom.GetSize() % split != 0)
+        count = dicom.GetSize() / split + 1;
+      else
+        count = dicom.GetSize() / split;
+    
+      std::vector<TransferBucket> b;
+      s.ComputePullBuckets(b, 1, split, "http://localhost/", BucketCompression_None);
+      ASSERT_EQ(count, b.size());
+
+      size_t size = dicom.GetSize() / count;
+      size_t offset = 0;
+      for (size_t j = 0; j < count; j++)
+      {
+        ASSERT_EQ(1u, b[j].GetChunksCount());
+        ASSERT_EQ("dicom", b[j].GetChunkInstanceId(0));
+        ASSERT_EQ(offset, b[j].GetChunkOffset(0));
+        if (j + 1 != count)
+          ASSERT_EQ(size, b[j].GetChunkSize(0));
+        else
+          ASSERT_EQ(dicom.GetSize() - (count - 1) * size, b[j].GetChunkSize(0));
+        offset += b[j].GetChunkSize(0);
+      }
+    }
+  }
+}
+
+
+TEST(DownloadArea, Basic)
+{
+  using namespace OrthancPlugins;
+  
+  std::string s1 = "Hello";
+  std::string s2 = "Hello, World!";
+
+  std::string md1, md2;
+  Orthanc::Toolbox::ComputeMD5(md1, s1);
+  Orthanc::Toolbox::ComputeMD5(md2, s2);
+  
+  std::vector<DicomInstanceInfo> instances;
+  instances.push_back(DicomInstanceInfo("d1", s1.size(), md1));
+  instances.push_back(DicomInstanceInfo("d2", s2.size(), md2));
+
+  {
+    DownloadArea area(instances);
+    ASSERT_EQ(s1.size() + s2.size(), area.GetTotalSize());
+    ASSERT_THROW(area.CheckMD5(), Orthanc::OrthancException);
+
+    area.WriteInstance("d1", s1.c_str(), s1.size());
+    area.WriteInstance("d2", s2.c_str(), s2.size());
+  
+    area.CheckMD5();
+  }
+
+  {
+    DownloadArea area(instances);
+    ASSERT_THROW(area.CheckMD5(), Orthanc::OrthancException);
+
+    {
+      TransferBucket b;
+      b.AddChunk(instances[0] /*d1*/, 0, 2);
+      area.WriteBucket(b, s1.c_str(), 2, BucketCompression_None);
+    }
+
+    {
+      TransferBucket b;
+      b.AddChunk(instances[0] /*d1*/, 2, 3);
+      b.AddChunk(instances[1] /*d2*/, 0, 4);
+      std::string s = s1.substr(2, 3) + s2.substr(0, 4);
+      area.WriteBucket(b, s.c_str(), s.size(), BucketCompression_None);
+    }
+
+    {
+      TransferBucket b;
+      b.AddChunk(instances[1] /*d2*/, 4, 9);
+      std::string s = s2.substr(4);
+      std::string t;
+      Orthanc::GzipCompressor compressor;
+      compressor.Compress(t, s.c_str(), s.size());
+      area.WriteBucket(b, t.c_str(), t.size(), BucketCompression_Gzip);
+    }
+
+    area.CheckMD5();
+  }
+}
+
+
+
+int main(int argc, char **argv)
+{
+  ::testing::InitGoogleTest(&argc, argv);
+  Orthanc::Logging::Initialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+  Orthanc::Logging::EnableTraceLevel(true);
+
+  int result = RUN_ALL_TESTS();
+
+  Orthanc::Logging::Finalize();
+
+  return result;
+}