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