metalink4-ruby 1.0.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/LICENSE +21 -0
- data/README.md +82 -0
- data/lib/metalink4.rb +607 -0
- data/metalink4-ruby.gemfile +19 -0
- data/rfc5854.txt +2187 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d43e73fcf1bfd375a6b933529250b3988afe90ef31c85d5f26e6758cf6a0a589
|
4
|
+
data.tar.gz: 165dc4c7395d9d7a52b25602c6f09aa93671d03075e982a30855e7474d5fbaa8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 16d8851908730101751a1925cd3577beb4a458dade1f205c393d97ec9d651a4ef728b86981aa27974b8087fd6a432e4c141076368747ffd3bf5f2ea2fcbfe0fe
|
7
|
+
data.tar.gz: 801830a5d7c08fa239f4a8a5f7fd5c20e1488c584d0da1a526bc396968d30a2fabe2fd2bfb021ada7449e8b4eda6a9cda0d239ab8b57a6e9e614f2c3519d7f0c
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Sudrien
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# metalink4-ruby
|
2
|
+
|
3
|
+
Class to format Metalink 4 / rfc5854 / .meta4 XML
|
4
|
+
|
5
|
+
|
6
|
+
All rfc5854 metadata is supported, as well as Chunk checksumming, for
|
7
|
+
detecting errors early on bad internet connections - see piece_size and
|
8
|
+
piece_count options.
|
9
|
+
|
10
|
+
All internally generated checksums are sha-256. Other checksum types
|
11
|
+
must be calculates externally and passed in.
|
12
|
+
|
13
|
+
|
14
|
+
No code taken from timsjoberg/metalink-ruby.
|
15
|
+
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Requirements:
|
20
|
+
|
21
|
+
- Ruby >= 1.9.3
|
22
|
+
|
23
|
+
``` sh
|
24
|
+
$ gem install metalink4-ruby
|
25
|
+
```
|
26
|
+
|
27
|
+
|
28
|
+
## Generation Examples
|
29
|
+
|
30
|
+
Please see https://github.com/Sudrien/metalink4-ruby/blob/main/test/test.rb
|
31
|
+
|
32
|
+
|
33
|
+
## Download Examples
|
34
|
+
|
35
|
+
metalink4-ruby 1.0.0 allows for the parsing of external meta4 files, but
|
36
|
+
does not support downloading and verifying checksums, as metalink supports
|
37
|
+
the listing of many protocols.
|
38
|
+
|
39
|
+
A presumed outdated list of clients is available at http://www.metalinker.org/implementation.html
|
40
|
+
|
41
|
+
Here is a partial list for convienece of easily scriptable programs for
|
42
|
+
convienence.
|
43
|
+
|
44
|
+
### curl
|
45
|
+
As of 7.78.0, curl removed Metalink support, rather than support a 'failure'
|
46
|
+
over a 'warning' when checksums did not match.
|
47
|
+
|
48
|
+
See: https://curl.se/docs/CVE-2021-22922.html
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
### aria2c example (recommended)
|
54
|
+
(Note: It is possible to compile binaries without metalink support)
|
55
|
+
|
56
|
+
Show file list
|
57
|
+
> aria2c test/1MB.meta4 -S
|
58
|
+
|
59
|
+
Download single file on that list
|
60
|
+
> aria2c test/1MB.meta4 --select-file=1
|
61
|
+
|
62
|
+
Check integrity of any files on disk instead of overwriting
|
63
|
+
> aria2c test/1MB.meta4 --check-integrity=true
|
64
|
+
|
65
|
+
More:
|
66
|
+
* https://aria2.github.io/manual/en/html/aria2c.html#bittorrent-metalink-options
|
67
|
+
* https://aria2.github.io/manual/en/html/aria2c.html#metalink-specific-options
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
### wget example (partial support)
|
73
|
+
(Note: It is possible to compile binaries without metalink support)
|
74
|
+
|
75
|
+
Download all files? 1MB.meta4.#1 a result, not obvious how to get correct filename
|
76
|
+
> wget --input-metalink=test/1MB.meta4
|
77
|
+
|
78
|
+
Download single file on that list, supposedly
|
79
|
+
> wget --input-metalink=test/1MB.meta4 --metalink-index=1
|
80
|
+
|
81
|
+
More:
|
82
|
+
* https://www.gnu.org/software/wget/manual/html_node/Logging-and-Input-File-Options.html
|
data/lib/metalink4.rb
ADDED
@@ -0,0 +1,607 @@
|
|
1
|
+
|
2
|
+
require 'builder'
|
3
|
+
require 'pathname'
|
4
|
+
require 'digest'
|
5
|
+
require 'mime/types'
|
6
|
+
require 'time'
|
7
|
+
require 'nokogiri'
|
8
|
+
require 'uri'
|
9
|
+
|
10
|
+
##
|
11
|
+
# Because hash can't just be a string. We need to specify what type.
|
12
|
+
|
13
|
+
class Metalink4FileHash
|
14
|
+
attr_accessor :hash_value,
|
15
|
+
:hash_type,
|
16
|
+
:piece
|
17
|
+
|
18
|
+
def initialize(opts = {})
|
19
|
+
opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
|
20
|
+
|
21
|
+
self.hash_value = opts.fetch(:hash_value, nil)
|
22
|
+
self.hash_type = opts.fetch(:hash_type, nil)
|
23
|
+
self.piece = opts.fetch(:piece, nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Hash must be in hexidecimal, lowercase
|
28
|
+
#
|
29
|
+
def hash_value=(v)
|
30
|
+
raise ("Improper Format '%s'" % v) if v && v !~ /[0-9a-f]+/
|
31
|
+
@hash_value = v
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# sha-256
|
36
|
+
#
|
37
|
+
def hash_type=(v)
|
38
|
+
@hash_type = v
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# nil if of whole file.
|
43
|
+
#
|
44
|
+
def piece=(v)
|
45
|
+
@piece = v
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
##
|
53
|
+
# Describes the download urls of files listed in the Metalink 4 file
|
54
|
+
class Metalink4FileUrl
|
55
|
+
attr_accessor :url,
|
56
|
+
:priority,
|
57
|
+
:location
|
58
|
+
##
|
59
|
+
# Options: url, location, priority.
|
60
|
+
# if only a string it profided it must be the URL.
|
61
|
+
def initialize(opts = {})
|
62
|
+
|
63
|
+
self.priority = 1
|
64
|
+
case opts
|
65
|
+
when String
|
66
|
+
self.url = opts
|
67
|
+
when Hash
|
68
|
+
opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
|
69
|
+
self.url = opts[:url]
|
70
|
+
self.location = opts[:location]
|
71
|
+
self.priority = opts[:priority]
|
72
|
+
else
|
73
|
+
raise "URL format %s" % opts.inspect
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# URI of remote file, HTTPS, HTTP, FTP, Bittorrent, etc.
|
79
|
+
# One, Required.
|
80
|
+
def url=(v)
|
81
|
+
@url = v ? URI.parse(v) : nil
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# ISO3166-1 2 character country code
|
86
|
+
# Client may ignore this field if a country code is not explicitly specified.
|
87
|
+
# One, optional.
|
88
|
+
def location=(v)
|
89
|
+
raise "Improper format" if v && v.to_s !~ /[a-z]{2}/
|
90
|
+
@location = v
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Priority this url is to be considered in. 1 is top level, 999999 is absolulte last. Duplicates allowed.
|
95
|
+
# Defaults to 1 if not specified.
|
96
|
+
def priority=(v)
|
97
|
+
@priority = [[v.to_i, 1].max, 999999].min
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Fragement call for builder.
|
102
|
+
# For internal use.
|
103
|
+
def render(builder_metalink_file, local_path)
|
104
|
+
begin
|
105
|
+
if MIME::Types.type_for(local_path.to_s).first == MIME::Types.type_for( self.url.path ).first
|
106
|
+
builder_metalink_file.url(
|
107
|
+
self.url.to_s, {
|
108
|
+
location: self.location,
|
109
|
+
priority: self.priority || 1,
|
110
|
+
}.delete_if{ |k, v| v.nil? }
|
111
|
+
)
|
112
|
+
else
|
113
|
+
builder_metalink_file.metaurl(
|
114
|
+
self.url.to_s, {
|
115
|
+
priority: self.priority || 1,
|
116
|
+
mediatype: self.url.path =~ /\.torrent/ ? "torrent" : MIME::Types.type_for( self.url.path ).first.to_s
|
117
|
+
}.delete_if{ |k, v| v.nil? }
|
118
|
+
)
|
119
|
+
end
|
120
|
+
rescue
|
121
|
+
puts [local_path, self.url]
|
122
|
+
throw [local_path, self.url]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
#Describes the files listed in the Metalink 4 file
|
129
|
+
class Metalink4File
|
130
|
+
|
131
|
+
attr_accessor :local_path,
|
132
|
+
:copyright,
|
133
|
+
:description,
|
134
|
+
:identity,
|
135
|
+
:language,
|
136
|
+
:logo,
|
137
|
+
:os,
|
138
|
+
:urls,
|
139
|
+
:publisher_name,
|
140
|
+
:publisher_url,
|
141
|
+
:signature,
|
142
|
+
:version,
|
143
|
+
:piece_size,
|
144
|
+
:piece_count,
|
145
|
+
:hashes,
|
146
|
+
:size
|
147
|
+
|
148
|
+
##
|
149
|
+
# Options: local_path, copyright, description, identity, language, logo, os, urls, publisher_name, publisher_url, signature, version, piece_size, piece_count
|
150
|
+
def initialize(opts = {})
|
151
|
+
opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
|
152
|
+
|
153
|
+
self.language = []
|
154
|
+
self.os = []
|
155
|
+
|
156
|
+
|
157
|
+
self.local_path = opts.fetch(:local_path, nil)
|
158
|
+
self.copyright = opts.fetch(:copyright, nil)
|
159
|
+
self.description = opts.fetch(:description, nil)
|
160
|
+
self.identity = opts.fetch(:identity, nil)
|
161
|
+
case opts.fetch(:language, nil)
|
162
|
+
when Array
|
163
|
+
self.language = opts.fetch(:language, nil)
|
164
|
+
when String
|
165
|
+
self.language = [opts.fetch(:language, nil)]
|
166
|
+
end
|
167
|
+
|
168
|
+
self.logo = opts.fetch(:logo, nil)
|
169
|
+
case opts.fetch(:os, nil)
|
170
|
+
when Array
|
171
|
+
self.os = opts.fetch(:os, nil)
|
172
|
+
when String
|
173
|
+
self.os = [opts.fetch(:os, nil)]
|
174
|
+
end
|
175
|
+
|
176
|
+
self.urls = []
|
177
|
+
opts.fetch(:urls, []).each do |url|
|
178
|
+
self.urls << Metalink4FileUrl.new(url)
|
179
|
+
end
|
180
|
+
|
181
|
+
self.publisher_name = opts.fetch(:publisher_name, nil)
|
182
|
+
self.publisher_url = opts.fetch(:publisher_url, nil)
|
183
|
+
self.signature = opts.fetch(:signature, nil)
|
184
|
+
self.version = opts.fetch(:version, nil)
|
185
|
+
|
186
|
+
self.piece_size = opts.fetch(:piece_size, nil)
|
187
|
+
self.piece_count = opts.fetch(:piece_count, nil)
|
188
|
+
|
189
|
+
self.hashes = []
|
190
|
+
end
|
191
|
+
|
192
|
+
##
|
193
|
+
# Path of local instance of file, relative to current working directory.
|
194
|
+
# relative path may be reproduced on the client side. MUST NOT include '.' or '..'
|
195
|
+
# Must not include an absolute path ( begin with / or drive letter )
|
196
|
+
#
|
197
|
+
# use File.chdir if you must to achive this
|
198
|
+
# Required
|
199
|
+
def local_path=(v)
|
200
|
+
return unless v
|
201
|
+
@local_path = Pathname.new(v)
|
202
|
+
raise "No absolute paths" if @local_path.absolute?
|
203
|
+
raise "No dots" if @local_path.to_s =~ /\.\/|\.\\/
|
204
|
+
@local_path
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
##
|
209
|
+
# The copyright of the file, human readble. URL should be ok, Or a full text.
|
210
|
+
# Lack of this field does not assert ANY paticular state of copyright.
|
211
|
+
# One, optional.
|
212
|
+
def copyright=(v)
|
213
|
+
@copyright = v
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
##
|
218
|
+
# if "Firefox 3.5" the description would be "A Web Browser"
|
219
|
+
# Human readable.
|
220
|
+
# One, optional, but reccomended.
|
221
|
+
def description=(v)
|
222
|
+
@description = v
|
223
|
+
end
|
224
|
+
|
225
|
+
##
|
226
|
+
# if "Firefox 3.5" the identity would be "Firefox"
|
227
|
+
# Human readable.
|
228
|
+
# One, optional.
|
229
|
+
def identity=(v)
|
230
|
+
@identity = v
|
231
|
+
end
|
232
|
+
|
233
|
+
##
|
234
|
+
# The language supported by the file.
|
235
|
+
# In rfc5646 format
|
236
|
+
# See: https://datatracker.ietf.org/doc/html/rfc5646
|
237
|
+
# One or more, optional.
|
238
|
+
def language=(v)
|
239
|
+
@language = v.is_a?(String) ? [v] : v
|
240
|
+
end
|
241
|
+
|
242
|
+
##
|
243
|
+
# Logo is the URL of an image file associated with this file. This can be an icon or avatar.
|
244
|
+
# It should be square, and support low resolutions.
|
245
|
+
# One, optional.
|
246
|
+
def logo=(v)
|
247
|
+
@logo = v ? URI.parse(v) : nil
|
248
|
+
end
|
249
|
+
|
250
|
+
##
|
251
|
+
# Operating System this download supports. One or more, optional.
|
252
|
+
# See IANA registry named "Operating System Names"
|
253
|
+
# at https://www.iana.org/assignments/operating-system-names/operating-system-names.xhtml
|
254
|
+
def os=(v)
|
255
|
+
@os = v.is_a?(String) ? [v] : v
|
256
|
+
end
|
257
|
+
|
258
|
+
##
|
259
|
+
# The creator of this meta4 file, which may be differnt than the files refrenced within.
|
260
|
+
# One, Optional.
|
261
|
+
def publisher_name=(v)
|
262
|
+
@publisher_name = v
|
263
|
+
end
|
264
|
+
|
265
|
+
##
|
266
|
+
# THE URL of the publisher, a descriptive page, NOT the source of this .meta4 file.
|
267
|
+
# One, Optional.
|
268
|
+
def publisher_url=(v)
|
269
|
+
@publisher_url = v ? URI.parse(v) : nil
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
##
|
274
|
+
# OpenPGP Or somthing. The signature should match the referenced file.
|
275
|
+
# One, Optional.
|
276
|
+
def signature=(v)
|
277
|
+
@signature = v
|
278
|
+
end
|
279
|
+
|
280
|
+
##
|
281
|
+
# if "Firefox 3.5" the version would be "3.5"
|
282
|
+
# Human readable.
|
283
|
+
# One, optional.
|
284
|
+
def version=(v)
|
285
|
+
@version = v
|
286
|
+
end
|
287
|
+
|
288
|
+
##
|
289
|
+
# Overwrite any present hashes. If no path is provided, checksum will be attempted of local_file.
|
290
|
+
# This might require a Dir.chdir.
|
291
|
+
#
|
292
|
+
# There is no protection of what this path can call - please sanitize any input to make sure files
|
293
|
+
# outside your project can't be checksummed.
|
294
|
+
#
|
295
|
+
# options: debug - show full path on error dump, piece_size - bytes, integer, OR piece_count - integer
|
296
|
+
#
|
297
|
+
# These hashes will be SHA256
|
298
|
+
def checksum!(path = nil, opts = {})
|
299
|
+
opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
|
300
|
+
|
301
|
+
raise "Path uses differnt extension" if path && File.extname(path) != File.extname(self.local_path)
|
302
|
+
|
303
|
+
|
304
|
+
if opts[:debug]
|
305
|
+
raise ("File '%s' does not exist" % File.expand_path( path ) ) if path && !File.exist?( File.expand_path( path ) )
|
306
|
+
else
|
307
|
+
raise ("File '%s' does not exist" % path ) if path && !File.exist?( File.expand_path( path ) )
|
308
|
+
end
|
309
|
+
|
310
|
+
raise "Can not specify both piece_size and piece_count" if opts[:piece_size] && opts[:piece_count]
|
311
|
+
raise "piece_size must be an Integer" if opts[:piece_size] && !opts[:piece_size].is_a?(Integer)
|
312
|
+
raise "piece_count must be an Integer" if opts[:piece_count] && !opts[:piece_count].is_a?(Integer)
|
313
|
+
|
314
|
+
path ||= self.local_path
|
315
|
+
|
316
|
+
#get filesize
|
317
|
+
self.size = File.size( File.expand_path( path ) )
|
318
|
+
|
319
|
+
# Overrides piece_count if set
|
320
|
+
# minimum size of 1KB for a piece
|
321
|
+
if opts[:piece_size]
|
322
|
+
@piece_count = nil
|
323
|
+
@piece_size = [opts[:piece_size], 1024].max
|
324
|
+
end
|
325
|
+
|
326
|
+
|
327
|
+
# Overrides piece_size if set
|
328
|
+
# This will be a multiple of 1024, and the last file will be slightly smaller
|
329
|
+
# this means 1KB is the minimum size for a piece
|
330
|
+
if opts[:piece_count]
|
331
|
+
@piece_count = opts[:piece_count]
|
332
|
+
@piece_size = ((@size / @piece_count) / 1024.0).ceil * 1024
|
333
|
+
end
|
334
|
+
|
335
|
+
self.hashes = []
|
336
|
+
self.hashes << Metalink4FileHash.new( hash_value: Digest::SHA256.file( File.expand_path( path ) ).hexdigest, hash_type: "sha-256" )
|
337
|
+
if self.piece_size
|
338
|
+
i = 0
|
339
|
+
(0...@size).step(self.piece_size).each do |offset|
|
340
|
+
self.hashes << Metalink4FileHash.new( hash_value: Digest::SHA256.hexdigest(File.read(File.expand_path( path ), self.piece_size, offset)), hash_type: "sha-256", piece: i )
|
341
|
+
i += 1
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
|
347
|
+
##
|
348
|
+
# Fragement call for builder.
|
349
|
+
# For internal use.
|
350
|
+
def render(builder_metalink, metalink4)
|
351
|
+
|
352
|
+
raise "Local path required" if !@local_path
|
353
|
+
#raise ("Local path '%s' does not match any files" % @local_path) if File.exist?(@local_path) || !self.hashes.empty?
|
354
|
+
|
355
|
+
#build up hashes if none were imported
|
356
|
+
if self.hashes.empty? && File.exist?(@local_path)
|
357
|
+
self.checksum!
|
358
|
+
end
|
359
|
+
|
360
|
+
metalink4.file( name: self.local_path ) do |metalink_file|
|
361
|
+
metalink_file.copyright( ("\n%s\n" % self.copyright) ) if self.copyright
|
362
|
+
metalink_file.description( self.description ) if self.description
|
363
|
+
metalink_file.identity( self.identity ) if self.identity
|
364
|
+
|
365
|
+
self.hashes.select{ |x| x.piece.nil? }.each do |hash|
|
366
|
+
metalink_file.hash(hash.hash_value, type: hash.hash_type)
|
367
|
+
end
|
368
|
+
|
369
|
+
#multiple languages possible
|
370
|
+
self.language.each do |language|
|
371
|
+
metalink_file.language( language )
|
372
|
+
end if self.language
|
373
|
+
|
374
|
+
metalink_file.logo( self.logo ) if self.logo
|
375
|
+
|
376
|
+
#multiple operating systems possible
|
377
|
+
self.os.each do |os|
|
378
|
+
metalink_file.os( os )
|
379
|
+
end if self.os
|
380
|
+
|
381
|
+
self.urls.each do |file_url|
|
382
|
+
file_url.render(
|
383
|
+
metalink_file,
|
384
|
+
self.local_path
|
385
|
+
)
|
386
|
+
end if self.urls
|
387
|
+
|
388
|
+
metalink_file.pieces(length: self.piece_size, type: self.hashes.select{ |x| x.piece }.first.hash_type) do |pieces|
|
389
|
+
self.hashes.select{ |x| x.piece }.sort_by(&:piece).each do |hash|
|
390
|
+
metalink_file.hash(hash.hash_value)
|
391
|
+
end
|
392
|
+
end if self.hashes.any?{ |x| x.piece }
|
393
|
+
|
394
|
+
metalink_file.publisher( name: self.publisher_name, url: self.publisher_url.to_s) if self.publisher_name || self.publisher_url
|
395
|
+
|
396
|
+
#what other type is it going to be
|
397
|
+
metalink_file.signature( ("\n%s\n" % self.signature), mediatype: "application/pgp-signature" ) if self.signature
|
398
|
+
|
399
|
+
metalink_file.size( self.size ) #SHOULD, file bytesize
|
400
|
+
metalink_file.version( self.version ) if self.version
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
|
405
|
+
|
406
|
+
##
|
407
|
+
# One of two options to enable piece hashes
|
408
|
+
# Overrides piece_count if set
|
409
|
+
# minimum size of 1KB for a piece
|
410
|
+
def piece_size=(v)
|
411
|
+
return unless v
|
412
|
+
@piece_count = nil
|
413
|
+
@piece_size = [v, 1024].max
|
414
|
+
end
|
415
|
+
|
416
|
+
private
|
417
|
+
|
418
|
+
##
|
419
|
+
# private becaue checksum! is a preferred call
|
420
|
+
# One of two options to enable piece hashes
|
421
|
+
# Overrides piece_size if set
|
422
|
+
# This will be a multiple of 1024, and the last file will be slightly smaller
|
423
|
+
# this means 1KB is the minimum size for a piece
|
424
|
+
def piece_count=(v)
|
425
|
+
return if v.nil?
|
426
|
+
@piece_count = v
|
427
|
+
@piece_size = ((@local_path.size / v) / 1024.0).ceil * 1024
|
428
|
+
end
|
429
|
+
|
430
|
+
|
431
|
+
end
|
432
|
+
|
433
|
+
##
|
434
|
+
# Describes the base Metalink 4 file as specified by rfc5854
|
435
|
+
# If served, it should have a header of application/metalink4+xml
|
436
|
+
# https://en.wikipedia.org/wiki/Metalink
|
437
|
+
class Metalink4
|
438
|
+
|
439
|
+
attr_accessor :files,
|
440
|
+
:published,
|
441
|
+
:updated,
|
442
|
+
:origin,
|
443
|
+
:origin_dynamic,
|
444
|
+
:xml
|
445
|
+
|
446
|
+
##
|
447
|
+
# Options: files, published, updated, origin, origin_dynamic
|
448
|
+
def initialize(opts = {})
|
449
|
+
opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
|
450
|
+
|
451
|
+
self.files = []
|
452
|
+
|
453
|
+
opts.fetch(:files, []).each do |file|
|
454
|
+
self.files << Metalink4File.new(file)
|
455
|
+
end
|
456
|
+
|
457
|
+
self.published = opts.fetch(:published, nil)
|
458
|
+
self.updated = opts.fetch(:updated, nil)
|
459
|
+
self.origin = opts.fetch(:origin, nil)
|
460
|
+
self.origin_dynamic = opts.fetch(:origin_dynamic, false)
|
461
|
+
|
462
|
+
self.xml = Builder::XmlMarkup.new(indent: 2)
|
463
|
+
end
|
464
|
+
|
465
|
+
##
|
466
|
+
# original publish date. Equivelent of ActiveRecord's created_at
|
467
|
+
# One, Optional.
|
468
|
+
def published=(v)
|
469
|
+
case v
|
470
|
+
when Time, nil
|
471
|
+
@published = v
|
472
|
+
when String
|
473
|
+
@published = Time.parse(v)
|
474
|
+
else
|
475
|
+
raise "Not a Time"
|
476
|
+
end
|
477
|
+
@published
|
478
|
+
end
|
479
|
+
|
480
|
+
##
|
481
|
+
# last publish date. Equivelent of ActiveRecord's updated_at
|
482
|
+
# One, Optional.
|
483
|
+
def updated=(v)
|
484
|
+
case v
|
485
|
+
when Time, nil
|
486
|
+
@updated = v
|
487
|
+
when String
|
488
|
+
@updated = Time.parse(v)
|
489
|
+
else
|
490
|
+
raise "Not a Time"
|
491
|
+
end
|
492
|
+
@updated
|
493
|
+
end
|
494
|
+
|
495
|
+
##
|
496
|
+
# url this meta4 file was made available at. Updates are potentially at the same url.
|
497
|
+
# One, Optional, but should.
|
498
|
+
def origin=(v)
|
499
|
+
@origin = v ? URI.parse(v) : nil
|
500
|
+
end
|
501
|
+
|
502
|
+
##
|
503
|
+
# if the meta4 file may have an update available
|
504
|
+
# Defaults to false.
|
505
|
+
def origin_dynamic=(v)
|
506
|
+
@origin = !!v
|
507
|
+
end
|
508
|
+
|
509
|
+
|
510
|
+
##
|
511
|
+
# Render to XML, returns string.
|
512
|
+
#
|
513
|
+
# Checksums are calculated at this point. ONLY sha-256 is calculated.
|
514
|
+
def render
|
515
|
+
|
516
|
+
raise "files not specified" if self.files.empty?
|
517
|
+
|
518
|
+
self.xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
519
|
+
self.xml.metalink( xmlns: "urn:ietf:params:xml:ns:metalink" ) do |metalink|
|
520
|
+
|
521
|
+
metalink.generator("Sudrien/metalink-ruby 0.2.0") #may
|
522
|
+
metalink.origin( self.origin, dynamic: self.origin_dynamic ) if self.origin
|
523
|
+
|
524
|
+
metalink.published( self.published.strftime('%FT%T%:z') ) if self.published
|
525
|
+
metalink.updated( self.updated.strftime('%FT%T%:z') ) if self.updated
|
526
|
+
|
527
|
+
self.files.each do |file|
|
528
|
+
file.render(self, metalink)
|
529
|
+
end
|
530
|
+
end
|
531
|
+
self.xml.target!
|
532
|
+
end
|
533
|
+
|
534
|
+
##
|
535
|
+
# Read a .meta4 to internal model. Takes either a file path or XML sting.
|
536
|
+
#
|
537
|
+
# Will import any provided hashes, so nothing needs to be calculated.
|
538
|
+
def self.read(potential_file_path)
|
539
|
+
|
540
|
+
begin
|
541
|
+
if File.exist?(potential_file_path)
|
542
|
+
doc = File.open(potential_file_path) { |f| Nokogiri::XML(f) }
|
543
|
+
elsif potential_file_path.is_a?(String)
|
544
|
+
doc = Nokogiri::XML(potential_file_path)
|
545
|
+
end
|
546
|
+
rescue
|
547
|
+
raise "%s Not an XML File" % potential_file_path
|
548
|
+
end
|
549
|
+
|
550
|
+
ret = Metalink4.new
|
551
|
+
ret.published = doc.at("metalink > published").content
|
552
|
+
ret.updated = doc.at("metalink > updated").content rescue nil
|
553
|
+
ret.origin = doc.at("metalink > origin").content rescue nil
|
554
|
+
ret.origin_dynamic = doc.at("metalink > origin[dynamic]").content == "true" rescue false
|
555
|
+
|
556
|
+
doc.search("metalink > file").each do |file|
|
557
|
+
|
558
|
+
ret.files << Metalink4File.new
|
559
|
+
|
560
|
+
ret.files.last.local_path = file.attr("name")
|
561
|
+
|
562
|
+
ret.files.last.copyright = file.at("copyright").content rescue nil
|
563
|
+
ret.files.last.description = file.at("description").content rescue nil
|
564
|
+
ret.files.last.identity = file.at("identity").content rescue nil
|
565
|
+
ret.files.last.language = file.at("language").content rescue nil
|
566
|
+
ret.files.last.logo = file.at("logo").content rescue nil
|
567
|
+
ret.files.last.os = file.at("os").content rescue nil
|
568
|
+
ret.files.last.publisher_name = file.at("publisher[name]").content rescue nil
|
569
|
+
ret.files.last.publisher_url = file.at("publisher[url]").content rescue nil
|
570
|
+
ret.files.last.signature = file.at("signature").content rescue nil
|
571
|
+
ret.files.last.version = file.at("version").content rescue nil
|
572
|
+
|
573
|
+
(file.search("hash") - file.search("pieces > hash")).each do |hash|
|
574
|
+
ret.files.last.hashes << Metalink4FileHash.new
|
575
|
+
ret.files.last.hashes.last.hash_value = hash.content rescue nil
|
576
|
+
ret.files.last.hashes.last.hash_type = hash.attr("type") rescue nil
|
577
|
+
end
|
578
|
+
|
579
|
+
ret.files.last.piece_size = file.at("pieces").attr("length").to_i rescue nil
|
580
|
+
piece_type = file.at("pieces").attr("type") rescue nil
|
581
|
+
|
582
|
+
|
583
|
+
ret.files.last.hashes ||= []
|
584
|
+
file.search("pieces > hash").each_with_index do |hash, hash_piece_index|
|
585
|
+
ret.files.last.hashes << Metalink4FileHash.new
|
586
|
+
ret.files.last.hashes.last.hash_value = (hash.content rescue nil)
|
587
|
+
ret.files.last.hashes.last.hash_type = piece_type
|
588
|
+
ret.files.last.hashes.last.piece = hash_piece_index
|
589
|
+
end
|
590
|
+
|
591
|
+
file.search("url").each do |url|
|
592
|
+
ret.files.last.urls << Metalink4FileUrl.new
|
593
|
+
ret.files.last.urls.last.url = url.content rescue nil
|
594
|
+
ret.files.last.urls.last.location = url.attr("location") rescue nil
|
595
|
+
ret.files.last.urls.last.priority = url.attr("priority") rescue nil
|
596
|
+
end
|
597
|
+
|
598
|
+
file.search("metaurl").each do |metaurl|
|
599
|
+
ret.files.last.urls << Metalink4FileUrl.new
|
600
|
+
ret.files.last.urls.last.url = metaurl.content rescue nil
|
601
|
+
ret.files.last.urls.last.priority = metaurl.attr("priority") rescue nil
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
ret
|
606
|
+
end
|
607
|
+
end
|