neocoin-mechanize 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. data/.autotest +6 -0
  2. data/.gemtest +0 -0
  3. data/CHANGELOG.rdoc +638 -0
  4. data/EXAMPLES.rdoc +187 -0
  5. data/FAQ.rdoc +11 -0
  6. data/GUIDE.rdoc +163 -0
  7. data/LICENSE.rdoc +20 -0
  8. data/Manifest.txt +172 -0
  9. data/README.rdoc +63 -0
  10. data/Rakefile +36 -0
  11. data/examples/flickr_upload.rb +22 -0
  12. data/examples/mech-dump.rb +5 -0
  13. data/examples/proxy_req.rb +7 -0
  14. data/examples/rubyforge.rb +20 -0
  15. data/examples/spider.rb +21 -0
  16. data/lib/mechanize.rb +662 -0
  17. data/lib/mechanize/content_type_error.rb +14 -0
  18. data/lib/mechanize/cookie.rb +85 -0
  19. data/lib/mechanize/cookie_jar.rb +241 -0
  20. data/lib/mechanize/element_matcher.rb +35 -0
  21. data/lib/mechanize/file.rb +80 -0
  22. data/lib/mechanize/file_connection.rb +17 -0
  23. data/lib/mechanize/file_request.rb +26 -0
  24. data/lib/mechanize/file_response.rb +74 -0
  25. data/lib/mechanize/file_saver.rb +37 -0
  26. data/lib/mechanize/form.rb +478 -0
  27. data/lib/mechanize/form/button.rb +9 -0
  28. data/lib/mechanize/form/check_box.rb +11 -0
  29. data/lib/mechanize/form/field.rb +44 -0
  30. data/lib/mechanize/form/file_upload.rb +23 -0
  31. data/lib/mechanize/form/image_button.rb +20 -0
  32. data/lib/mechanize/form/multi_select_list.rb +83 -0
  33. data/lib/mechanize/form/option.rb +49 -0
  34. data/lib/mechanize/form/radio_button.rb +48 -0
  35. data/lib/mechanize/form/select_list.rb +40 -0
  36. data/lib/mechanize/headers.rb +25 -0
  37. data/lib/mechanize/history.rb +83 -0
  38. data/lib/mechanize/http.rb +3 -0
  39. data/lib/mechanize/http/agent.rb +738 -0
  40. data/lib/mechanize/inspect.rb +88 -0
  41. data/lib/mechanize/monkey_patch.rb +37 -0
  42. data/lib/mechanize/page.rb +408 -0
  43. data/lib/mechanize/page/base.rb +8 -0
  44. data/lib/mechanize/page/frame.rb +27 -0
  45. data/lib/mechanize/page/image.rb +30 -0
  46. data/lib/mechanize/page/label.rb +20 -0
  47. data/lib/mechanize/page/link.rb +82 -0
  48. data/lib/mechanize/page/meta_refresh.rb +56 -0
  49. data/lib/mechanize/pluggable_parsers.rb +101 -0
  50. data/lib/mechanize/redirect_limit_reached_error.rb +16 -0
  51. data/lib/mechanize/redirect_not_get_or_head_error.rb +19 -0
  52. data/lib/mechanize/response_code_error.rb +22 -0
  53. data/lib/mechanize/response_read_error.rb +27 -0
  54. data/lib/mechanize/robots_disallowed_error.rb +29 -0
  55. data/lib/mechanize/unsupported_scheme_error.rb +8 -0
  56. data/lib/mechanize/util.rb +113 -0
  57. data/test/data/htpasswd +1 -0
  58. data/test/data/server.crt +16 -0
  59. data/test/data/server.csr +12 -0
  60. data/test/data/server.key +15 -0
  61. data/test/data/server.pem +15 -0
  62. data/test/helper.rb +175 -0
  63. data/test/htdocs/alt_text.html +10 -0
  64. data/test/htdocs/bad_form_test.html +9 -0
  65. data/test/htdocs/button.jpg +0 -0
  66. data/test/htdocs/canonical_uri.html +9 -0
  67. data/test/htdocs/dir with spaces/foo.html +1 -0
  68. data/test/htdocs/empty_form.html +6 -0
  69. data/test/htdocs/file_upload.html +26 -0
  70. data/test/htdocs/find_link.html +41 -0
  71. data/test/htdocs/form_multi_select.html +16 -0
  72. data/test/htdocs/form_multival.html +37 -0
  73. data/test/htdocs/form_no_action.html +18 -0
  74. data/test/htdocs/form_no_input_name.html +16 -0
  75. data/test/htdocs/form_select.html +16 -0
  76. data/test/htdocs/form_select_all.html +16 -0
  77. data/test/htdocs/form_select_none.html +17 -0
  78. data/test/htdocs/form_select_noopts.html +10 -0
  79. data/test/htdocs/form_set_fields.html +14 -0
  80. data/test/htdocs/form_test.html +188 -0
  81. data/test/htdocs/frame_referer_test.html +10 -0
  82. data/test/htdocs/frame_test.html +30 -0
  83. data/test/htdocs/google.html +13 -0
  84. data/test/htdocs/iframe_test.html +16 -0
  85. data/test/htdocs/index.html +6 -0
  86. data/test/htdocs/link with space.html +5 -0
  87. data/test/htdocs/meta_cookie.html +11 -0
  88. data/test/htdocs/no_title_test.html +6 -0
  89. data/test/htdocs/nofollow.html +9 -0
  90. data/test/htdocs/noindex.html +9 -0
  91. data/test/htdocs/norobots.html +8 -0
  92. data/test/htdocs/rails_3_encoding_hack_form_test.html +27 -0
  93. data/test/htdocs/rel_nofollow.html +8 -0
  94. data/test/htdocs/relative/tc_relative_links.html +21 -0
  95. data/test/htdocs/robots.html +8 -0
  96. data/test/htdocs/robots.txt +2 -0
  97. data/test/htdocs/tc_bad_charset.html +9 -0
  98. data/test/htdocs/tc_bad_links.html +5 -0
  99. data/test/htdocs/tc_base_images.html +10 -0
  100. data/test/htdocs/tc_base_link.html +8 -0
  101. data/test/htdocs/tc_blank_form.html +11 -0
  102. data/test/htdocs/tc_charset.html +6 -0
  103. data/test/htdocs/tc_checkboxes.html +19 -0
  104. data/test/htdocs/tc_encoded_links.html +5 -0
  105. data/test/htdocs/tc_field_precedence.html +11 -0
  106. data/test/htdocs/tc_follow_meta.html +8 -0
  107. data/test/htdocs/tc_form_action.html +48 -0
  108. data/test/htdocs/tc_images.html +8 -0
  109. data/test/htdocs/tc_links.html +18 -0
  110. data/test/htdocs/tc_meta_in_body.html +9 -0
  111. data/test/htdocs/tc_no_attributes.html +16 -0
  112. data/test/htdocs/tc_pretty_print.html +17 -0
  113. data/test/htdocs/tc_radiobuttons.html +17 -0
  114. data/test/htdocs/tc_referer.html +16 -0
  115. data/test/htdocs/tc_relative_links.html +19 -0
  116. data/test/htdocs/tc_textarea.html +23 -0
  117. data/test/htdocs/test_bad_encoding.html +52 -0
  118. data/test/htdocs/test_click.html +11 -0
  119. data/test/htdocs/unusual______.html +5 -0
  120. data/test/servlets.rb +402 -0
  121. data/test/ssl_server.rb +48 -0
  122. data/test/test_cookies.rb +129 -0
  123. data/test/test_form_action.rb +52 -0
  124. data/test/test_form_as_hash.rb +59 -0
  125. data/test/test_form_button.rb +46 -0
  126. data/test/test_frames.rb +34 -0
  127. data/test/test_headers.rb +33 -0
  128. data/test/test_history.rb +118 -0
  129. data/test/test_history_added.rb +16 -0
  130. data/test/test_html_unscape_forms.rb +46 -0
  131. data/test/test_if_modified_since.rb +20 -0
  132. data/test/test_images.rb +19 -0
  133. data/test/test_mechanize.rb +842 -0
  134. data/test/test_mechanize_cookie.rb +345 -0
  135. data/test/test_mechanize_cookie_jar.rb +401 -0
  136. data/test/test_mechanize_file.rb +53 -0
  137. data/test/test_mechanize_file_request.rb +19 -0
  138. data/test/test_mechanize_file_response.rb +21 -0
  139. data/test/test_mechanize_form.rb +576 -0
  140. data/test/test_mechanize_form_check_box.rb +37 -0
  141. data/test/test_mechanize_form_encoding.rb +120 -0
  142. data/test/test_mechanize_form_field.rb +21 -0
  143. data/test/test_mechanize_form_image_button.rb +12 -0
  144. data/test/test_mechanize_form_textarea.rb +51 -0
  145. data/test/test_mechanize_http_agent.rb +697 -0
  146. data/test/test_mechanize_link.rb +84 -0
  147. data/test/test_mechanize_page_encoding.rb +147 -0
  148. data/test/test_mechanize_page_link.rb +382 -0
  149. data/test/test_mechanize_page_meta_refresh.rb +115 -0
  150. data/test/test_mechanize_redirect_not_get_or_head_error.rb +18 -0
  151. data/test/test_mechanize_subclass.rb +22 -0
  152. data/test/test_mechanize_util.rb +92 -0
  153. data/test/test_multi_select.rb +118 -0
  154. data/test/test_no_attributes.rb +13 -0
  155. data/test/test_option.rb +18 -0
  156. data/test/test_pluggable_parser.rb +136 -0
  157. data/test/test_post_form.rb +37 -0
  158. data/test/test_pretty_print.rb +22 -0
  159. data/test/test_radiobutton.rb +75 -0
  160. data/test/test_redirect_limit_reached.rb +39 -0
  161. data/test/test_referer.rb +81 -0
  162. data/test/test_relative_links.rb +40 -0
  163. data/test/test_request.rb +13 -0
  164. data/test/test_response_code.rb +53 -0
  165. data/test/test_robots.rb +72 -0
  166. data/test/test_save_file.rb +48 -0
  167. data/test/test_scheme.rb +48 -0
  168. data/test/test_select.rb +119 -0
  169. data/test/test_select_all.rb +15 -0
  170. data/test/test_select_none.rb +15 -0
  171. data/test/test_select_noopts.rb +18 -0
  172. data/test/test_set_fields.rb +44 -0
  173. data/test/test_ssl_server.rb +20 -0
  174. metadata +354 -0
@@ -0,0 +1,14 @@
1
+ class Mechanize
2
+ # =Synopsis
3
+ # This class contains an error for when a pluggable parser tries to
4
+ # parse a content type that it does not know how to handle. For example
5
+ # if Mechanize::Page were to try to parse a PDF, a ContentTypeError
6
+ # would be thrown.
7
+ class ContentTypeError < Mechanize::Error
8
+ attr_reader :content_type
9
+
10
+ def initialize(content_type)
11
+ @content_type = content_type
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,85 @@
1
+ require 'time'
2
+ require 'webrick/cookie'
3
+
4
+ # This class is used to represent an HTTP Cookie.
5
+ class Mechanize::Cookie < WEBrick::Cookie
6
+
7
+ attr_accessor :session
8
+
9
+ def self.parse(uri, str, log = Mechanize.log)
10
+ return str.split(/,(?=[^;,]*=)|,$/).map { |c|
11
+ cookie_elem = c.split(/;+/)
12
+ first_elem = cookie_elem.shift
13
+ first_elem.strip!
14
+ key, value = first_elem.split(/\=/, 2)
15
+
16
+ cookie = nil
17
+ begin
18
+ cookie = new(key, value.dup)
19
+ rescue
20
+ log.warn("Couldn't parse key/value: #{first_elem}") if log
21
+ end
22
+
23
+ next unless cookie
24
+
25
+ cookie_elem.each do |pair|
26
+ pair.strip!
27
+ key, value = pair.split(/\=/, 2)
28
+ next unless key
29
+ value = WEBrick::HTTPUtils.dequote(value.strip) if value
30
+
31
+ case key.downcase
32
+ when "domain" then
33
+ value = ".#{value}" unless value =~ /^\./
34
+ cookie.domain = value
35
+ when "path" then
36
+ cookie.path = value
37
+ when 'expires'
38
+ if value.empty? then
39
+ cookie.session = true
40
+ next
41
+ end
42
+
43
+ begin
44
+ cookie.expires = Time::parse(value)
45
+ rescue
46
+ log.warn("Couldn't parse expires: #{value}") if log
47
+ end
48
+ when "max-age" then
49
+ begin
50
+ cookie.max_age = Integer(value)
51
+ rescue
52
+ log.warn("Couldn't parse max age '#{value}'") if log
53
+ cookie.max_age = nil
54
+ end
55
+ when "comment" then cookie.comment = value
56
+ when "version" then
57
+ begin
58
+ cookie.version = Integer(value)
59
+ rescue
60
+ log.warn("Couldn't parse version '#{value}'") if log
61
+ cookie.version = nil
62
+ end
63
+ when "secure" then cookie.secure = true
64
+ end
65
+ end
66
+
67
+ cookie.path ||= uri.path.to_s.sub(%r%[^/]*$%, '')
68
+ cookie.secure ||= false
69
+ cookie.domain ||= uri.host
70
+ # Move this in to the cookie jar
71
+ yield cookie if block_given?
72
+
73
+ cookie
74
+ }
75
+ end
76
+
77
+ def expired?
78
+ return false unless expires
79
+ Time.now > expires
80
+ end
81
+
82
+ def to_s
83
+ "#{@name}=#{@value}"
84
+ end
85
+ end
@@ -0,0 +1,241 @@
1
+ ##
2
+ # This class is used to manage the Cookies that have been returned from
3
+ # any particular website.
4
+
5
+ class Mechanize::CookieJar
6
+
7
+ # add_cookie wants something resembling a URI.
8
+
9
+ FakeURI = Struct.new(:host) # :nodoc:
10
+
11
+ attr_reader :jar
12
+
13
+ def initialize
14
+ @jar = {}
15
+ end
16
+
17
+ def initialize_copy other # :nodoc:
18
+ @jar = Marshal.load Marshal.dump other.jar
19
+ end
20
+
21
+ # Add a cookie to the Jar.
22
+ def add(uri, cookie)
23
+ return unless valid_cookie_for_uri?(uri, cookie)
24
+
25
+ normal_domain = cookie.domain.downcase
26
+
27
+ @jar[normal_domain] ||= {} unless @jar.has_key?(normal_domain)
28
+
29
+ @jar[normal_domain][cookie.path] ||= {}
30
+ @jar[normal_domain][cookie.path][cookie.name] = cookie
31
+
32
+ cookie
33
+ end
34
+
35
+ # Fetch the cookies that should be used for the URI object passed in.
36
+ def cookies(url)
37
+ cleanup
38
+ url.path = '/' if url.path.empty?
39
+
40
+ domains = @jar.find_all { |domain, _|
41
+ cookie_domain = self.class.strip_port(domain)
42
+ if cookie_domain.start_with?('.')
43
+ url.host =~ /#{Regexp.escape cookie_domain}$/i
44
+ else
45
+ url.host =~ /^#{Regexp.escape cookie_domain}$/i
46
+ end
47
+ }
48
+
49
+ return [] unless domains.length > 0
50
+
51
+ cookies = domains.map { |_,paths|
52
+ paths.find_all { |path, _|
53
+ url.path =~ /^#{Regexp.escape(path)}/
54
+ }.map { |_,cookie| cookie.values }
55
+ }.flatten
56
+
57
+ cookies.find_all { |cookie| ! cookie.expired? }
58
+ end
59
+
60
+ def empty?(url)
61
+ cookies(url).length > 0 ? false : true
62
+ end
63
+
64
+ def to_a
65
+ cleanup
66
+
67
+ @jar.map do |domain, paths|
68
+ paths.map do |path, names|
69
+ names.values
70
+ end
71
+ end.flatten
72
+ end
73
+
74
+ # Save the cookie jar to a file in the format specified.
75
+ #
76
+ # Available formats:
77
+ # :yaml <- YAML structure
78
+ # :cookiestxt <- Mozilla's cookies.txt format
79
+ def save_as(file, format = :yaml)
80
+ jar = dup
81
+ jar.cleanup true
82
+
83
+ open(file, 'w') { |f|
84
+ case format
85
+ when :yaml then
86
+ load_yaml
87
+
88
+ YAML.dump(jar.jar, f)
89
+ when :cookiestxt then
90
+ jar.dump_cookiestxt(f)
91
+ else
92
+ raise ArgumentError, "Unknown cookie jar file format"
93
+ end
94
+ }
95
+
96
+ self
97
+ end
98
+
99
+ # Load cookie jar from a file in the format specified.
100
+ #
101
+ # Available formats:
102
+ # :yaml <- YAML structure.
103
+ # :cookiestxt <- Mozilla's cookies.txt format
104
+ def load(file, format = :yaml)
105
+ @jar = open(file) { |f|
106
+ case format
107
+ when :yaml then
108
+ load_yaml
109
+
110
+ YAML.load(f)
111
+ when :cookiestxt then
112
+ load_cookiestxt(f)
113
+ else
114
+ raise ArgumentError, "Unknown cookie jar file format"
115
+ end
116
+ }
117
+
118
+ cleanup
119
+
120
+ self
121
+ end
122
+
123
+ def load_yaml # :nodoc:
124
+ begin
125
+ require 'psych'
126
+ rescue LoadError
127
+ end
128
+
129
+ require 'yaml'
130
+ end
131
+
132
+ # Clear the cookie jar
133
+ def clear!
134
+ @jar = {}
135
+ end
136
+
137
+ # Read cookies from Mozilla cookies.txt-style IO stream
138
+ def load_cookiestxt(io)
139
+ now = Time.now
140
+
141
+ io.each_line do |line|
142
+ line.chomp!
143
+ line.gsub!(/#.+/, '')
144
+ fields = line.split("\t")
145
+
146
+ next if fields.length != 7
147
+
148
+ expires_seconds = fields[4].to_i
149
+ expires = (expires_seconds == 0) ? nil : Time.at(expires_seconds)
150
+ next if expires and (expires < now)
151
+
152
+ c = Mechanize::Cookie.new(fields[5], fields[6])
153
+ c.domain = fields[0]
154
+ # Field 1 indicates whether the cookie can be read by other machines at
155
+ # the same domain. This is computed by the cookie implementation, based
156
+ # on the domain value.
157
+ c.path = fields[2] # Path for which the cookie is relevant
158
+ c.secure = (fields[3] == "TRUE") # Requires a secure connection
159
+ c.expires = expires # Time the cookie expires.
160
+ c.version = 0 # Conforms to Netscape cookie spec.
161
+
162
+ add(FakeURI.new(c.domain), c)
163
+ end
164
+
165
+ @jar
166
+ end
167
+
168
+ # Write cookies to Mozilla cookies.txt-style IO stream
169
+ def dump_cookiestxt(io)
170
+ to_a.each do |cookie|
171
+ fields = []
172
+ fields[0] = cookie.domain
173
+
174
+ if cookie.domain =~ /^\./
175
+ fields[1] = "TRUE"
176
+ else
177
+ fields[1] = "FALSE"
178
+ end
179
+
180
+ fields[2] = cookie.path
181
+
182
+ if cookie.secure == true
183
+ fields[3] = "TRUE"
184
+ else
185
+ fields[3] = "FALSE"
186
+ end
187
+
188
+ fields[4] = cookie.expires.to_i.to_s
189
+
190
+ fields[5] = cookie.name
191
+ fields[6] = cookie.value
192
+ io.puts(fields.join("\t"))
193
+ end
194
+ end
195
+
196
+ private
197
+ # Determine if the cookie's domain and path are valid for
198
+ # the uri.host based on the rules in RFC 2965
199
+ def valid_cookie_for_uri?(uri, cookie)
200
+ cookie_domain = self.class.strip_port(cookie.domain)
201
+
202
+ # reject cookies whose domains do not contain an embedded dot
203
+ # cookies for localhost and .local. are exempt from this rule
204
+ return false if
205
+ cookie_domain !~ /.\../ && cookie_domain !~ /(localhost|\.?local)\.?$/
206
+
207
+ cookie_domain = if cookie_domain.start_with? '.' then
208
+ ".?#{Regexp.escape cookie_domain[1..-1]}"
209
+ else
210
+ Regexp.escape cookie_domain
211
+ end
212
+
213
+ # Permitted: A Set-Cookie for x.foo.com for Domain=.foo.com
214
+ # Not Permitted: A Set-Cookie for y.x.foo.com for Domain=.foo.com because
215
+ # y.x contains a dot
216
+ # Not Permitted: A Set-Cookie for foo.com for Domain=.bar.com
217
+ match = uri.host.match(/#{cookie_domain}/i)
218
+ return false if match.nil? || match.pre_match =~ /.\../
219
+
220
+ true
221
+ end
222
+
223
+ protected
224
+
225
+ # Remove expired cookies
226
+ def cleanup session = false
227
+ @jar.each do |domain, paths|
228
+ paths.each do |path, names|
229
+ names.each do |cookie_name, cookie|
230
+ paths[path].delete(cookie_name) if
231
+ cookie.expired? or (session and cookie.session)
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ def self.strip_port(host)
238
+ host.gsub(/:[0-9]+$/,'')
239
+ end
240
+ end
241
+
@@ -0,0 +1,35 @@
1
+ module Mechanize::ElementMatcher
2
+
3
+ def elements_with singular, plural = "#{singular}s"
4
+ class_eval <<-CODE
5
+ def #{plural}_with criteria = {}
6
+ criteria = if String === criteria then
7
+ {:name => criteria}
8
+ else
9
+ criteria.map do |k, v|
10
+ k = :dom_id if k.to_sym == :id
11
+ [k, v]
12
+ end
13
+ end
14
+
15
+ f = #{plural}.find_all do |thing|
16
+ criteria.all? do |k,v|
17
+ v === thing.send(k)
18
+ end
19
+ end
20
+ yield f if block_given?
21
+ f
22
+ end
23
+
24
+ def #{singular}_with criteria = {}
25
+ f = #{plural}_with(criteria).first
26
+ yield f if block_given?
27
+ f
28
+ end
29
+
30
+ alias :#{singular} :#{singular}_with
31
+ CODE
32
+ end
33
+
34
+ end
35
+
@@ -0,0 +1,80 @@
1
+ class Mechanize
2
+ # = Synopsis
3
+ # This is the default (and base) class for the Pluggable Parsers. If
4
+ # Mechanize cannot find an appropriate class to use for the content type,
5
+ # this class will be used. For example, if you download a JPG, Mechanize
6
+ # will not know how to parse it, so this class will be instantiated.
7
+ #
8
+ # This is a good class to use as the base class for building your own
9
+ # pluggable parsers.
10
+ #
11
+ # == Example
12
+ # require 'rubygems'
13
+ # require 'mechanize'
14
+ #
15
+ # agent = Mechanize.new
16
+ # agent.get('http://example.com/foo.jpg').class #=> Mechanize::File
17
+ #
18
+ class File
19
+ extend Forwardable
20
+
21
+ attr_accessor :uri, :response, :body, :code, :filename
22
+ alias :header :response
23
+ def_delegator :header, :[], :[]
24
+ def_delegator :header, :[]=, :[]=
25
+ def_delegator :header, :key?, :key?
26
+ def_delegator :header, :each, :each
27
+ def_delegator :header, :canonical_each, :canonical_each
28
+
29
+ alias :content :body
30
+
31
+ def initialize(uri=nil, response=nil, body=nil, code=nil)
32
+ @uri = uri
33
+ @body = body
34
+ @code = code
35
+ @response = Headers.new
36
+
37
+ # Copy the headers in to a hash to prevent memory leaks
38
+ if response
39
+ response.each { |k,v|
40
+ @response[k] = v
41
+ }
42
+ end
43
+
44
+ @filename = 'index.html'
45
+
46
+ # Set the filename
47
+ if disposition = @response['content-disposition']
48
+ disposition.split(/;\s*/).each do |pair|
49
+ k,v = pair.split(/=/, 2)
50
+ @filename = v if k && k.downcase == 'filename'
51
+ end
52
+ else
53
+ if @uri
54
+ @filename = @uri.path.split(/\//).last || 'index.html'
55
+ @filename << ".html" unless @filename =~ /\./
56
+ end
57
+ end
58
+
59
+ yield self if block_given?
60
+ end
61
+
62
+ # Use this method to save the content of this object to filename
63
+ def save_as(filename = nil)
64
+ if filename.nil?
65
+ filename = @filename
66
+ number = 1
67
+ while(::File.exists?(filename))
68
+ filename = "#{@filename}.#{number}"
69
+ number += 1
70
+ end
71
+ end
72
+
73
+ ::File::open(filename, "wb") { |f|
74
+ f.write body
75
+ }
76
+ end
77
+
78
+ alias :save :save_as
79
+ end
80
+ end