resync-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +42 -0
  3. data/.rubocop.yml +23 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +2 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.md +22 -0
  8. data/README.md +59 -0
  9. data/Rakefile +56 -0
  10. data/example.rb +49 -0
  11. data/lib/resync/client/bitstream.rb +79 -0
  12. data/lib/resync/client/client.rb +58 -0
  13. data/lib/resync/client/downloadable.rb +35 -0
  14. data/lib/resync/client/dump.rb +25 -0
  15. data/lib/resync/client/http_helper.rb +111 -0
  16. data/lib/resync/client/resync_extensions.rb +85 -0
  17. data/lib/resync/client/version.rb +6 -0
  18. data/lib/resync/client/zip_package.rb +66 -0
  19. data/lib/resync/client.rb +3 -0
  20. data/resync-client.gemspec +35 -0
  21. data/spec/data/examples/capability-list.xml +25 -0
  22. data/spec/data/examples/change-dump-manifest.xml +41 -0
  23. data/spec/data/examples/change-dump.xml +41 -0
  24. data/spec/data/examples/change-list-index.xml +22 -0
  25. data/spec/data/examples/change-list.xml +36 -0
  26. data/spec/data/examples/resource-dump-manifest.xml +25 -0
  27. data/spec/data/examples/resource-dump.xml +39 -0
  28. data/spec/data/examples/resource-list-index.xml +21 -0
  29. data/spec/data/examples/resource-list.xml +24 -0
  30. data/spec/data/examples/source-description.xml +25 -0
  31. data/spec/data/resourcedump/manifest.xml +18 -0
  32. data/spec/data/resourcedump/resourcedump.xml +16 -0
  33. data/spec/data/resourcedump/resourcedump.zip +0 -0
  34. data/spec/data/resourcedump/resources/res1 +7 -0
  35. data/spec/data/resourcedump/resources/res2 +7 -0
  36. data/spec/rspec_custom_matchers.rb +11 -0
  37. data/spec/spec_helper.rb +31 -0
  38. data/spec/todo.rb +65 -0
  39. data/spec/unit/resync/client/bitstream_spec.rb +90 -0
  40. data/spec/unit/resync/client/client_spec.rb +130 -0
  41. data/spec/unit/resync/client/dump_spec.rb +26 -0
  42. data/spec/unit/resync/client/http_helper_spec.rb +235 -0
  43. data/spec/unit/resync/client/resync_extensions_spec.rb +148 -0
  44. data/spec/unit/resync/client/zip_package_spec.rb +44 -0
  45. metadata +238 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 339acc57f21bd27647e8f9484157b6b4d6f34007
4
+ data.tar.gz: 293b87bfd6ec673c9e01701d1976626aef06994b
5
+ SHA512:
6
+ metadata.gz: 5b30f4db7f6ff8c3a7c8652edc4458dc991593f6e1d14ccf49e7e7d28d16f03fdb94853a24d70ff57d4e32fb927ef447e0fd78145a442b41533e9b11668188d4
7
+ data.tar.gz: f37a86a97b4b3018d79b6a64031f3a0e36f6d5a165a19ff8a88284c490061d0876cfea283989321c3f4b7837e2203d262dff22d57796c793c981791bc5368cc0
data/.gitignore ADDED
@@ -0,0 +1,42 @@
1
+ # Ruby defaults
2
+
3
+ /.bundle/
4
+ /.yardoc
5
+ /Gemfile.lock
6
+ /_yardoc/
7
+ /coverage/
8
+ /doc/
9
+ /pkg/
10
+ /spec/reports/
11
+ /tmp/
12
+ *.bundle
13
+ *.so
14
+ *.o
15
+ *.a
16
+ mkmf.log
17
+
18
+ # Rails engine
19
+
20
+ .bundle/
21
+ log/*.log
22
+ spec/dummy/db/*.sqlite3
23
+ spec/dummy/db/*.sqlite3-journal
24
+ spec/dummy/log/*.log
25
+ spec/dummy/tmp/
26
+ spec/dummy/.sass-cache
27
+
28
+ # IntellJ
29
+
30
+ *.iml
31
+ *.ipr
32
+ *.iws
33
+ .rakeTasks
34
+ .idea
35
+
36
+ # Emacs
37
+
38
+ *~
39
+
40
+ # Mac OS
41
+
42
+ .DS_Store
data/.rubocop.yml ADDED
@@ -0,0 +1,23 @@
1
+ # Disable line-length check; it's too easy for the cure to be worse than the disease
2
+ Metrics/LineLength:
3
+ Enabled: False
4
+
5
+ # Disable problematic module documentation check (see https://github.com/bbatsov/rubocop/issues/947)
6
+ Style/Documentation:
7
+ Enabled: false
8
+
9
+ # Allow one line around class body (Style/EmptyLines will still disallow two or more)
10
+ Style/EmptyLinesAroundClassBody:
11
+ Enabled: false
12
+
13
+ # Allow one line around module body (Style/EmptyLines will still disallow two or more)
14
+ Style/EmptyLinesAroundModuleBody:
15
+ Enabled: false
16
+
17
+ # Allow one line around block body (Style/EmptyLines will still disallow two or more)
18
+ Style/EmptyLinesAroundBlockBody:
19
+ Enabled: false
20
+
21
+ # Allow %r notation for regexes with a single / character
22
+ Style/RegexpLiteral:
23
+ MaxSlashes: 0
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.2
data/.travis.yml ADDED
@@ -0,0 +1,2 @@
1
+ language: ruby
2
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 The Regents of the University of California
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # resync-client
2
+
3
+ A gem providing a Ruby client for the [ResourceSync](http://www.openarchives.org/rs/1.0/resourcesync) web synchronization framework, based on the [resync](https://github.com/dmolesUC3/resync) gem and [Net::HTTP](http://ruby-doc.org/stdlib-2.2.2/libdoc/net/http/rdoc/Net/HTTP.html).
4
+
5
+ ## Status
6
+
7
+ This is a work in progress. We welcome bug reports and feature requests.
8
+
9
+ ## Usage
10
+
11
+ Retrieving the [Source Description](http://www.openarchives.org/rs/1.0/resourcesync#wellknown) for a site:
12
+
13
+ ```ruby
14
+ client = Resync::Client.new
15
+
16
+ source_desc_uri = 'http://example.org/.well-known/resourcesync'
17
+ source_desc = client.get_and_parse(source_desc_uri) # => Resync::SourceDescription
18
+ ```
19
+
20
+ Retrieving a [Capability List](http://www.openarchives.org/rs/1.0/resourcesync#CapabilityList) from the source description:
21
+
22
+ ```ruby
23
+ cap_list_resource = source_desc.resource_for(capability: 'capabilitylist')
24
+ cap_list = cap_list_resource.get_and_parse # => Resync::CapabilityList
25
+ ```
26
+
27
+ Retrieving a [Change List](http://www.openarchives.org/rs/1.0/resourcesync#ChangeList) and downloading the latest revision of a known resource to a file
28
+
29
+ ```ruby
30
+ change_list_resource = cap_list.resource_for(capability: 'changelist')
31
+ change_list = change_list_resource.get_and_parse # => Resync::ChangeList
32
+ latest_rev_resource = change_list.latest_for(uri: URI('http://example.com/my-resource'))
33
+ latest_rev_resource.download_to_file('/tmp/my-resource.txt')
34
+ ```
35
+
36
+ Retrieving a [Change Dump](http://www.openarchives.org/rs/1.0/resourcesync#ChangeDump), searching through its manifests for changes to a specified URL, downloading the ZIP package containing that resource, and extracting it from the ZIP package:
37
+
38
+ ```ruby
39
+ change_dump_resource = cap_list.resource_for(capability: 'changedump')
40
+ change_dump = change_dump_resource.get_and_parse # => Resync::ChangeDump
41
+ change_dump.resources.each do |package|
42
+ manifest_link = package.link_for(rel: 'contents')
43
+ if manifest_link
44
+ manifest = manifest_link.get_and_parse # => Resync::ChangeDumpManifest
45
+ latest_resource = manifest.latest_for(uri: URI('http://example.com/my-resource'))
46
+ if latest_resource
47
+ timestamp = latest_resource.modified_time.strftime('%s%3N')
48
+ zip_package = package.zip_package # => Resync::ZipPackage (downloaded to temp file)
49
+ bitstream = zip_package.bitstream_for(latest_resource) # => Resync::Bitstream
50
+ content = bitstream.content # => String (extracted from ZIP file)
51
+ File.open("/tmp/my-resource-#{timestamp}.txt") { |f| f.write(content) }
52
+ end
53
+ end
54
+ end
55
+ ```
56
+
57
+ ## Limitations
58
+
59
+ `resync-client` hasn't really been tested except with [resync-simulator](https://github.com/resync/resync-simulator), and that not much beyond what you'll find in [example.rb](example.rb), so expect trouble.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ # ------------------------------------------------------------
2
+ # RSpec
3
+
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+
7
+ namespace :spec do
8
+
9
+ desc 'Run all unit tests'
10
+ RSpec::Core::RakeTask.new(:unit) do |task|
11
+ task.rspec_opts = %w(--color --format documentation --order default)
12
+ task.pattern = 'unit/**/*_spec.rb'
13
+ end
14
+
15
+ desc 'Run all acceptance tests'
16
+ RSpec::Core::RakeTask.new(:acceptance) do |task|
17
+ ENV['COVERAGE'] = nil
18
+ task.rspec_opts = %w(--color --format documentation --order default)
19
+ task.pattern = 'acceptance/**/*_spec.rb'
20
+ end
21
+
22
+ task all: [:unit, :acceptance]
23
+ end
24
+
25
+ desc 'Run all tests'
26
+ task spec: 'spec:all'
27
+
28
+ # ------------------------------------------------------------
29
+ # Coverage
30
+
31
+ desc 'Run all unit tests with coverage'
32
+ task :coverage do
33
+ ENV['COVERAGE'] = 'true'
34
+ Rake::Task['spec:unit'].execute
35
+ end
36
+
37
+ # ------------------------------------------------------------
38
+ # RuboCop
39
+
40
+ require 'rubocop/rake_task'
41
+ RuboCop::RakeTask.new
42
+
43
+ # ------------------------------------------------------------
44
+ # TODOs
45
+
46
+ desc 'List TODOs (from spec/todo.rb)'
47
+ RSpec::Core::RakeTask.new(:todo) do |task|
48
+ task.rspec_opts = %w(--color --format documentation --order default)
49
+ task.pattern = 'todo.rb'
50
+ end
51
+
52
+ # ------------------------------------------------------------
53
+ # Defaults
54
+
55
+ desc 'Run unit tests, check test coverage, run acceptance tests, check code style'
56
+ task default: [:coverage, 'spec:acceptance', :rubocop]
data/example.rb ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Note: This assumes we're running from the root of the resync-client project
4
+ $LOAD_PATH << File.dirname(__FILE__)
5
+ require 'lib/resync/client'
6
+
7
+ client = Resync::Client.new
8
+
9
+ # Note: this URI is from resync-simulator: https://github.com/resync/resync-simulator
10
+ source_desc_uri = 'http://localhost:8888/.well-known/resourcesync'
11
+ puts "Source: #{source_desc_uri}"
12
+ source_desc = client.get_and_parse(source_desc_uri) # Resync::SourceDescription
13
+
14
+ cap_list_resource = source_desc.resource_for(capability: 'capabilitylist')
15
+ cap_list = cap_list_resource.get_and_parse # Resync::CapabilityList
16
+
17
+ change_list_resource = cap_list.resource_for(capability: 'changelist')
18
+ change_list = change_list_resource.get_and_parse # Resync::ChangeList
19
+ puts " from: #{change_list.metadata.from_time}"
20
+ puts " until: #{change_list.metadata.until_time}"
21
+
22
+ changes = change_list.resources # Array<Resync::Resource>
23
+ puts " changes: #{changes.size}"
24
+ puts
25
+
26
+ n = changes.size > 5 ? 5 : changes.size
27
+ puts "last #{n} changes of any kind:"
28
+ changes.slice(-n, n).each do |r|
29
+ puts " #{r.uri}"
30
+ puts " modified at: #{r.modified_time}"
31
+ puts " change type: #{r.metadata.change}"
32
+ puts " md5: #{r.metadata.hash('md5')}"
33
+ end
34
+
35
+ last_update = changes.select { |r| r.metadata.change == Resync::Types::Change::UPDATED }[-1]
36
+ puts 'last update:'
37
+ puts " #{last_update.uri}"
38
+ puts " modified at: #{last_update.modified_time}"
39
+ puts " change type: #{last_update.metadata.change}"
40
+ puts " md5: #{last_update.metadata.hash('md5')}"
41
+
42
+ last_update_response = last_update.get
43
+ puts last_update_response.class
44
+ puts " content: #{last_update_response}"
45
+
46
+ last_update_file = last_update.download_to_temp_file
47
+ last_update_file_contents = File.read(last_update_file)
48
+ puts " as file: #{last_update_file}"
49
+ puts " file content: #{last_update_file_contents}"
@@ -0,0 +1,79 @@
1
+ module Resync
2
+
3
+ # A single entry in a ZIP package.
4
+ class Bitstream
5
+
6
+ # ------------------------------------------------------------
7
+ # Attributes
8
+
9
+ # @return [String] the path to the entry within the ZIP file
10
+ attr_reader :path
11
+
12
+ # @return [Resource] the resource describing this bitstream
13
+ attr_reader :resource
14
+
15
+ # @return [Metadata] the metadata for this bitstream
16
+ attr_reader :metadata
17
+
18
+ # ------------------------------------------------------------
19
+ # Initializer
20
+
21
+ # Creates a new bitstream for the specified resource.
22
+ #
23
+ # @param zipfile [Zip::File] The zipfile to read the bitstream from.
24
+ # @param resource [Resource] The resource describing the bitstream.
25
+ def initialize(zipfile:, resource:)
26
+ self.resource = resource
27
+ @zip_entry = zipfile.find_entry(@path)
28
+ end
29
+
30
+ # ------------------------------------------------------------
31
+ # Convenience accessors
32
+
33
+ # The (uncompressed) size of the bitstream.
34
+ def size
35
+ @size ||= @zip_entry.size
36
+ end
37
+
38
+ # The bitstream, as an +IO+-like object. Subsequent
39
+ # calls will return the same stream.
40
+ def stream
41
+ @stream ||= @zip_entry.get_input_stream
42
+ end
43
+
44
+ # The content of the bitstream. The content will be
45
+ # read only once.
46
+ def content
47
+ @content ||= stream.read
48
+ end
49
+
50
+ # The content type of the bitstream, as per {#metadata}.
51
+ def mime_type
52
+ @mime_type ||= metadata.mime_type
53
+ end
54
+
55
+ # ------------------------------------------------------------
56
+ # Private methods
57
+
58
+ private
59
+
60
+ def resource=(value)
61
+ fail ArgumentError, 'nil is not a resource' unless value
62
+ self.metadata = value.metadata
63
+ @resource = value
64
+ end
65
+
66
+ def metadata=(value)
67
+ fail 'no metadata found' unless value
68
+ self.path = value.path
69
+ @metadata = value
70
+ end
71
+
72
+ def path=(value)
73
+ fail 'no path found in metadata' unless value
74
+ @path = value.start_with?('/') ? value.slice(1..-1) : value
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,58 @@
1
+ require_relative 'version'
2
+ require_relative 'http_helper'
3
+
4
+ module Resync
5
+
6
+ # Utility class for retrieving HTTP content and parsing it as ResourceSync documents.
7
+ class Client
8
+
9
+ # ------------------------------------------------------------
10
+ # Initializer
11
+
12
+ # Creates a new +Client+
13
+ # @param helper [HTTPHelper] the HTTP helper. Defaults to a new HTTP helper with
14
+ # +resync-client VERSION+ as the User-Agent string.
15
+ def initialize(helper: HTTPHelper.new(user_agent: "resync-client #{VERSION}"))
16
+ @helper = helper
17
+ end
18
+
19
+ # ------------------------------------------------------------
20
+ # Public methods
21
+
22
+ # Gets the content of the specified URI and parses it as a ResourceSync document.
23
+ def get_and_parse(uri)
24
+ uri = Resync::XML.to_uri(uri)
25
+ raw_contents = get(uri)
26
+ doc = XMLParser.parse(raw_contents)
27
+ doc.client = self
28
+ doc
29
+ end
30
+
31
+ # Gets the content of the specified URI as a string.
32
+ # @param uri [URI, String] the URI to download
33
+ # @return [String] the content of the URI
34
+ def get(uri)
35
+ uri = Resync::XML.to_uri(uri)
36
+ @helper.fetch(uri: uri)
37
+ end
38
+
39
+ # Gets the content of the specified URI and saves it to a temporary file.
40
+ # @param uri [URI, String] the URI to download
41
+ # @return [String] the path to the downloaded file
42
+ def download_to_temp_file(uri)
43
+ uri = Resync::XML.to_uri(uri)
44
+ @helper.fetch_to_file(uri: uri)
45
+ end
46
+
47
+ # Gets the content of the specified URI and saves it to the specified file,
48
+ # overwriting it if it exists.
49
+ # @param uri [URI, String] the URI to download
50
+ # @param path [String] the path to save the download to
51
+ # @return [String] the path to the downloaded file
52
+ def download_to_file(uri:, path:)
53
+ uri = Resync::XML.to_uri(uri)
54
+ @helper.fetch_to_file(path: path, uri: uri)
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,35 @@
1
+ require 'resync'
2
+
3
+ module Resync
4
+
5
+ # Adds +get+, +get_raw+, and +get_file+ methods, delegating
6
+ # to the injected client.
7
+ #
8
+ # @see Augmented#client
9
+ module Downloadable
10
+ # Delegates to {Client#get_and_parse} to get the contents of
11
+ # +:uri+ as a ResourceSync document
12
+ def get_and_parse # rubocop:disable Style/AccessorMethodName
13
+ client.get_and_parse(uri)
14
+ end
15
+
16
+ # Delegates to {Client#get} to get the contents of this +:uri+
17
+ def get # rubocop:disable Style/AccessorMethodName
18
+ client.get(uri)
19
+ end
20
+
21
+ # Delegates to {Client#download_to_temp_file} to download the
22
+ # contents of +:uri+ to a file.
23
+ def download_to_temp_file # rubocop:disable Style/AccessorMethodName
24
+ client.download_to_temp_file(uri)
25
+ end
26
+
27
+ # Delegates to {Client#download_to_file} to download the
28
+ # contents of +:uri+ to the specified path.
29
+ # @param path [String] the path to download to
30
+ def download_to_file(path)
31
+ client.download_to_file(uri: uri, path: path)
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'zip_package'
2
+
3
+ module Resync
4
+ # Extends {ChangeDump} and {ResourceDump} to provide
5
+ # transparent access to the linked bitstream packages
6
+ module Dump
7
+ # Injects a +:zip_package+ method into each resource,
8
+ # downloading the (presumed) bitstream package to a
9
+ # temp file and returning it as a {ZipPackage}
10
+ def resources=(value)
11
+ super
12
+ resources.each do |r|
13
+ def r.zip_package
14
+ @zip_package ||= ZipPackage.new(download_to_temp_file)
15
+ end
16
+ end
17
+ end
18
+
19
+ # A list of the {ZipPackage}s for each resource
20
+ # @return [Array<ZipPackage>] the zip packages for each resource
21
+ def zip_packages
22
+ resources.map(&:zip_package)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,111 @@
1
+ require 'net/http'
2
+ require 'tempfile'
3
+ require 'uri'
4
+ require 'mime-types'
5
+
6
+ module Resync
7
+
8
+ # Utility class simplifying GET requests for HTTP/HTTPS resources.
9
+ #
10
+ class HTTPHelper
11
+
12
+ # ------------------------------------------------------------
13
+ # Constants
14
+
15
+ # The default number of redirects to follow before erroring out.
16
+ DEFAULT_MAX_REDIRECTS = 5
17
+
18
+ # ------------------------------------------------------------
19
+ # Accessors
20
+
21
+ # @!attribute [rw] user_agent
22
+ # @return [String] the User-Agent string to send when making requests
23
+ attr_accessor :user_agent
24
+
25
+ # @!attribute [rw] redirect_limit
26
+ # @return [Integer] the number of redirects to follow before erroring out
27
+ attr_accessor :redirect_limit
28
+
29
+ # ------------------------------------------------------------
30
+ # Initializer
31
+
32
+ # Creates a new +HTTPHelper+
33
+ #
34
+ # @param user_agent [String] the User-Agent string to send when making requests
35
+ # @param redirect_limit [Integer] the number of redirects to follow before erroring out
36
+ # (defaults to {DEFAULT_MAX_REDIRECTS})
37
+ def initialize(user_agent:, redirect_limit: DEFAULT_MAX_REDIRECTS)
38
+ @user_agent = user_agent
39
+ @redirect_limit = redirect_limit
40
+ end
41
+
42
+ # ------------------------------------------------------------
43
+ # Public methods
44
+
45
+ # Gets the content of the specified URI as a string.
46
+ # @param uri [URI] the URI to download
47
+ # @param limit [Integer] the number of redirects to follow (defaults to {#redirect_limit})
48
+ # @return [String] the content of the URI
49
+ def fetch(uri:, limit: redirect_limit)
50
+ make_request(uri, limit) do |success|
51
+ # not 100% clear why we need an explicit return here; it
52
+ # doesn't show up in unit tests but it does in example.rb
53
+ return success.body
54
+ end
55
+ end
56
+
57
+ # Gets the content of the specified URI and saves it to a file. If no
58
+ # file path is provided, saves it to a temporary file.
59
+ # @param uri [URI] the URI to download
60
+ # @param path [String] the path to save the download to (optional)
61
+ # @return [String] the path to the downloaded file
62
+ def fetch_to_file(uri:, path: nil, limit: redirect_limit)
63
+ make_request(uri, limit) do |success|
64
+ file = path ? File.new(path, 'w+') : Tempfile.new(['resync-client', ".#{extension_for(success)}"])
65
+ open file, 'w' do |out|
66
+ success.read_body { |chunk| out.write(chunk) }
67
+ end
68
+ return file.path
69
+ end
70
+ end
71
+
72
+ # ------------------------------------------------------------
73
+ # Private methods
74
+
75
+ private
76
+
77
+ def make_request(uri, limit, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
78
+ fail "Redirect limit (#{redirect_limit}) exceeded retrieving URI #{uri}" if limit <= 0
79
+ req = Net::HTTP::Get.new(uri, 'User-Agent' => user_agent)
80
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: (uri.scheme == 'https')) do |http|
81
+ http.request(req) do |response|
82
+ case response
83
+ when Net::HTTPSuccess
84
+ block.call(response)
85
+ when Net::HTTPInformation, Net::HTTPRedirection
86
+ make_request(redirect_uri_for(response, uri), limit - 1, &block)
87
+ else
88
+ fail "Error #{response.code}: #{response.message} retrieving URI #{uri}"
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def extension_for(response)
95
+ content_type = response['Content-Type']
96
+ mime_type = MIME::Types[content_type].first || MIME::Types['application/octet-stream'].first
97
+ mime_type.preferred_extension || 'bin'
98
+ end
99
+
100
+ def redirect_uri_for(response, original_uri)
101
+ if response.is_a?(Net::HTTPInformation)
102
+ original_uri
103
+ else
104
+ location = response['location']
105
+ new_uri = URI(location)
106
+ new_uri.relative? ? original_uri + location : new_uri
107
+ end
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,85 @@
1
+ require 'resync'
2
+ require_relative 'downloadable'
3
+ require_relative 'dump'
4
+
5
+ # Extensions to the core Resync classes to simplify retrieval
6
+ module Resync
7
+
8
+ # ------------------------------------------------------------
9
+ # Base classes
10
+
11
+ # Injects a {Client} that subclasses can use to fetch
12
+ # resources and links
13
+ #
14
+ # @!attribute [rw] client
15
+ # @return [Client] the injected {Client}. Defaults to
16
+ # a new {Client} instance.
17
+ class Augmented
18
+ attr_writer :client
19
+
20
+ def client
21
+ @client ||= Client.new
22
+ end
23
+
24
+ alias_method :base_links=, :links=
25
+ private :base_links=
26
+
27
+ # Adds a +:client+ method to each link, delegating
28
+ # to {#client}
29
+ def links=(value)
30
+ self.base_links = value
31
+ self.base_links = value
32
+ parent = self
33
+ links.each do |l|
34
+ l.define_singleton_method(:client) do
35
+ parent.client
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ # Adds a +:client+ method to each resource, delegating
42
+ # to {Augmented#client}
43
+ class BaseResourceList
44
+ alias_method :base_resources=, :resources=
45
+ private :base_resources=
46
+
47
+ # Adds a +:client+ method to each resource, delegating
48
+ # to {Augmented#client}
49
+ def resources=(value)
50
+ self.base_resources = value
51
+ parent = self
52
+ resources.each do |r|
53
+ r.define_singleton_method(:client) do
54
+ parent.client
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ # ------------------------------------------------------------
61
+ # Resource and Link
62
+
63
+ # Includes the {Downloadable} module
64
+ class Resource
65
+ include Downloadable
66
+ end
67
+
68
+ # Includes the {Link} module
69
+ class Link
70
+ include Downloadable
71
+ end
72
+
73
+ # ------------------------------------------------------------
74
+ # ResourceDump and ChaneDump
75
+
76
+ # Includes the {Dump} module
77
+ class ResourceDump
78
+ include Dump
79
+ end
80
+
81
+ # Includes the {Dump} module
82
+ class ChangeDump
83
+ include Dump
84
+ end
85
+ end
@@ -0,0 +1,6 @@
1
+ module Resync
2
+ class Client
3
+ # The version of this gem.
4
+ VERSION = '0.1.0'
5
+ end
6
+ end