resync-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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