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.
- checksums.yaml +7 -0
- data/.gitignore +42 -0
- data/.rubocop.yml +23 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -0
- data/Gemfile +3 -0
- data/LICENSE.md +22 -0
- data/README.md +59 -0
- data/Rakefile +56 -0
- data/example.rb +49 -0
- data/lib/resync/client/bitstream.rb +79 -0
- data/lib/resync/client/client.rb +58 -0
- data/lib/resync/client/downloadable.rb +35 -0
- data/lib/resync/client/dump.rb +25 -0
- data/lib/resync/client/http_helper.rb +111 -0
- data/lib/resync/client/resync_extensions.rb +85 -0
- data/lib/resync/client/version.rb +6 -0
- data/lib/resync/client/zip_package.rb +66 -0
- data/lib/resync/client.rb +3 -0
- data/resync-client.gemspec +35 -0
- data/spec/data/examples/capability-list.xml +25 -0
- data/spec/data/examples/change-dump-manifest.xml +41 -0
- data/spec/data/examples/change-dump.xml +41 -0
- data/spec/data/examples/change-list-index.xml +22 -0
- data/spec/data/examples/change-list.xml +36 -0
- data/spec/data/examples/resource-dump-manifest.xml +25 -0
- data/spec/data/examples/resource-dump.xml +39 -0
- data/spec/data/examples/resource-list-index.xml +21 -0
- data/spec/data/examples/resource-list.xml +24 -0
- data/spec/data/examples/source-description.xml +25 -0
- data/spec/data/resourcedump/manifest.xml +18 -0
- data/spec/data/resourcedump/resourcedump.xml +16 -0
- data/spec/data/resourcedump/resourcedump.zip +0 -0
- data/spec/data/resourcedump/resources/res1 +7 -0
- data/spec/data/resourcedump/resources/res2 +7 -0
- data/spec/rspec_custom_matchers.rb +11 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/todo.rb +65 -0
- data/spec/unit/resync/client/bitstream_spec.rb +90 -0
- data/spec/unit/resync/client/client_spec.rb +130 -0
- data/spec/unit/resync/client/dump_spec.rb +26 -0
- data/spec/unit/resync/client/http_helper_spec.rb +235 -0
- data/spec/unit/resync/client/resync_extensions_spec.rb +148 -0
- data/spec/unit/resync/client/zip_package_spec.rb +44 -0
- 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
data/Gemfile
ADDED
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
|