├── .github
└── workflows
│ └── ruby.yml
├── .gitignore
├── .rubocop.yml
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── MANIFEST
├── README.md
├── Rakefile
├── bin
└── gemirro
├── gemirro.gemspec
├── lib
├── gemirro.rb
└── gemirro
│ ├── cli.rb
│ ├── cli
│ ├── index.rb
│ ├── init.rb
│ ├── list.rb
│ ├── server.rb
│ └── update.rb
│ ├── configuration.rb
│ ├── gem.rb
│ ├── gem_version.rb
│ ├── gem_version_collection.rb
│ ├── gems_fetcher.rb
│ ├── http.rb
│ ├── indexer.rb
│ ├── mirror_directory.rb
│ ├── mirror_file.rb
│ ├── server.rb
│ ├── source.rb
│ ├── utils.rb
│ ├── version.rb
│ ├── versions_fetcher.rb
│ └── versions_file.rb
├── spec
├── fixtures
│ ├── gems
│ │ └── gemirro-0.0.1.gem
│ └── quick
│ │ └── gemirro-0.0.1.gemspec.rz
├── gemirro
│ ├── cli_spec.rb
│ ├── configuration_spec.rb
│ ├── gem_spec.rb
│ ├── gem_version_collection_spec.rb
│ ├── gem_version_spec.rb
│ ├── gems_fetcher_spec.rb
│ ├── http_spec.rb
│ ├── indexer_spec.rb
│ ├── mirror_directory_spec.rb
│ ├── mirror_file_spec.rb
│ ├── server_spec.rb
│ ├── source_spec.rb
│ ├── versions_fetcher_spec.rb
│ └── versions_file_spec.rb
└── spec_helper.rb
├── task
├── manifest.rake
├── rspec.rake
└── rubocop.rake
├── template
├── config.rb
├── logs
│ └── .gitkeep
└── public
│ ├── dist
│ └── css
│ │ └── gemirro.css
│ └── gems
│ └── .gitkeep
└── views
├── gem.erb
├── index.erb
├── layout.erb
└── not_found.erb
/.github/workflows/ruby.yml:
--------------------------------------------------------------------------------
1 | name: "Gemirro tests"
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | rspec:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | ruby: ['3.0', '3.1', '3.2']
11 | name: Rspec on Ruby ${{ matrix.ruby }}
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: ruby/setup-ruby@v1
15 | with:
16 | ruby-version: ${{ matrix.ruby }}
17 | bundler-cache: true
18 | - run: bundle exec rspec
19 |
20 | rubocop:
21 | runs-on: ubuntu-latest
22 | name: Run rubocop
23 | steps:
24 | - uses: actions/checkout@v4
25 | - uses: ruby/setup-ruby@v1
26 | with:
27 | ruby-version: '3.2'
28 | bundler-cache: true
29 | - run: bundle exec rubocop
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.project
2 | /coverage
3 | /pkg
4 | /.ruby-version
5 | /*.gem
6 | /.bundle
7 | .DS_Store
8 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | SuggestExtensions: false
3 | NewCops: enable
4 | TargetRubyVersion: 3.0
5 | Include:
6 | - '**/Gemfile'
7 | - lib/**/*.rb
8 | - spec/**/*.rb
9 | - gemirro.gemspec
10 | Exclude:
11 | - files/**/*
12 | - templates/**/*
13 | - spec/**/*
14 | - vendor/**/*
15 |
16 | Naming/FileName:
17 | Exclude:
18 | - Rakefile
19 | Metrics/MethodLength:
20 | Enabled: false
21 | Metrics/ClassLength:
22 | Enabled: false
23 | Metrics/CyclomaticComplexity:
24 | Enabled: false
25 | Metrics/PerceivedComplexity:
26 | Enabled: false
27 | Metrics/AbcSize:
28 | Enabled: false
29 | Metrics/BlockLength:
30 | Enabled: false
31 | Security/MarshalLoad:
32 | Enabled: false
33 | Style/ExpandPathArguments:
34 | Enabled: false
35 | Style/OptionalBooleanParameter:
36 | Enabled: false
37 | Lint/MissingSuper:
38 | Enabled: false
39 | Style/TrailingUnderscoreVariable:
40 | Enabled: false
41 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org/'
4 |
5 | gemspec
6 |
7 | group :development, :test do
8 | gem 'fakefs', '~> 2'
9 | gem 'rack-test', '~> 1.1'
10 | gem 'rake', '~> 13'
11 | gem 'rspec', '~> 3.10'
12 | gem 'rubocop', '~> 1'
13 | gem 'simplecov', '~> 0.21'
14 | end
15 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | gemirro (2.0.0)
5 | addressable (~> 2.8)
6 | builder (~> 3.2)
7 | compact_index (~> 0.15)
8 | confstruct (~> 1.1)
9 | erubis (~> 2.7)
10 | httpclient (~> 2.8)
11 | logger (> 0)
12 | mutex_m (> 0)
13 | ostruct (> 0)
14 | parallel (~> 1.21)
15 | rubygems-generate_index (> 0)
16 | sinatra (>= 3.1, < 4.0)
17 | slop (~> 3.6)
18 | stringio (> 0)
19 | thin (~> 1.8)
20 |
21 | GEM
22 | remote: https://rubygems.org/
23 | specs:
24 | addressable (2.8.7)
25 | public_suffix (>= 2.0.2, < 7.0)
26 | ast (2.4.2)
27 | base64 (0.2.0)
28 | builder (3.3.0)
29 | compact_index (0.15.0)
30 | confstruct (1.1.0)
31 | hashie (>= 3.3, < 5)
32 | daemons (1.4.1)
33 | diff-lcs (1.5.1)
34 | docile (1.4.1)
35 | erubis (2.7.0)
36 | eventmachine (1.2.7)
37 | fakefs (2.8.0)
38 | hashie (4.1.0)
39 | httpclient (2.8.3)
40 | json (2.9.1)
41 | language_server-protocol (3.17.0.3)
42 | logger (1.6.5)
43 | mustermann (3.0.3)
44 | ruby2_keywords (~> 0.0.1)
45 | mutex_m (0.3.0)
46 | ostruct (0.6.1)
47 | parallel (1.26.3)
48 | parser (3.3.7.0)
49 | ast (~> 2.4.1)
50 | racc
51 | public_suffix (6.0.1)
52 | racc (1.8.1)
53 | rack (2.2.14)
54 | rack-protection (3.2.0)
55 | base64 (>= 0.1.0)
56 | rack (~> 2.2, >= 2.2.4)
57 | rack-test (1.1.0)
58 | rack (>= 1.0, < 3)
59 | rainbow (3.1.1)
60 | rake (13.2.1)
61 | regexp_parser (2.10.0)
62 | rspec (3.13.0)
63 | rspec-core (~> 3.13.0)
64 | rspec-expectations (~> 3.13.0)
65 | rspec-mocks (~> 3.13.0)
66 | rspec-core (3.13.2)
67 | rspec-support (~> 3.13.0)
68 | rspec-expectations (3.13.3)
69 | diff-lcs (>= 1.2.0, < 2.0)
70 | rspec-support (~> 3.13.0)
71 | rspec-mocks (3.13.2)
72 | diff-lcs (>= 1.2.0, < 2.0)
73 | rspec-support (~> 3.13.0)
74 | rspec-support (3.13.2)
75 | rubocop (1.70.0)
76 | json (~> 2.3)
77 | language_server-protocol (>= 3.17.0)
78 | parallel (~> 1.10)
79 | parser (>= 3.3.0.2)
80 | rainbow (>= 2.2.2, < 4.0)
81 | regexp_parser (>= 2.9.3, < 3.0)
82 | rubocop-ast (>= 1.36.2, < 2.0)
83 | ruby-progressbar (~> 1.7)
84 | unicode-display_width (>= 2.4.0, < 4.0)
85 | rubocop-ast (1.37.0)
86 | parser (>= 3.3.1.0)
87 | ruby-progressbar (1.13.0)
88 | ruby2_keywords (0.0.5)
89 | rubygems-generate_index (1.1.3)
90 | compact_index (~> 0.15.0)
91 | simplecov (0.22.0)
92 | docile (~> 1.1)
93 | simplecov-html (~> 0.11)
94 | simplecov_json_formatter (~> 0.1)
95 | simplecov-html (0.13.1)
96 | simplecov_json_formatter (0.1.4)
97 | sinatra (3.2.0)
98 | mustermann (~> 3.0)
99 | rack (~> 2.2, >= 2.2.4)
100 | rack-protection (= 3.2.0)
101 | tilt (~> 2.0)
102 | slop (3.6.0)
103 | stringio (3.1.2)
104 | thin (1.8.2)
105 | daemons (~> 1.0, >= 1.0.9)
106 | eventmachine (~> 1.0, >= 1.0.4)
107 | rack (>= 1, < 3)
108 | tilt (2.6.0)
109 | unicode-display_width (3.1.4)
110 | unicode-emoji (~> 4.0, >= 4.0.4)
111 | unicode-emoji (4.0.4)
112 |
113 | PLATFORMS
114 | ruby
115 |
116 | DEPENDENCIES
117 | fakefs (~> 2)
118 | gemirro!
119 | rack-test (~> 1.1)
120 | rake (~> 13)
121 | rspec (~> 3.10)
122 | rubocop (~> 1)
123 | simplecov (~> 0.21)
124 |
125 | BUNDLED WITH
126 | 2.5.16
127 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/MANIFEST:
--------------------------------------------------------------------------------
1 | .github/workflows/ruby.yml
2 | .gitignore
3 | .rubocop.yml
4 | Gemfile
5 | Gemfile.lock
6 | LICENSE
7 | MANIFEST
8 | README.md
9 | Rakefile
10 | bin/gemirro
11 | gemirro.gemspec
12 | lib/gemirro.rb
13 | lib/gemirro/cli.rb
14 | lib/gemirro/cli/index.rb
15 | lib/gemirro/cli/init.rb
16 | lib/gemirro/cli/list.rb
17 | lib/gemirro/cli/server.rb
18 | lib/gemirro/cli/update.rb
19 | lib/gemirro/configuration.rb
20 | lib/gemirro/gem.rb
21 | lib/gemirro/gem_version.rb
22 | lib/gemirro/gem_version_collection.rb
23 | lib/gemirro/gems_fetcher.rb
24 | lib/gemirro/http.rb
25 | lib/gemirro/indexer.rb
26 | lib/gemirro/mirror_directory.rb
27 | lib/gemirro/mirror_file.rb
28 | lib/gemirro/server.rb
29 | lib/gemirro/source.rb
30 | lib/gemirro/utils.rb
31 | lib/gemirro/version.rb
32 | lib/gemirro/versions_fetcher.rb
33 | lib/gemirro/versions_file.rb
34 | spec/fixtures/gems/gemirro-0.0.1.gem
35 | spec/fixtures/quick/gemirro-0.0.1.gemspec.rz
36 | spec/gemirro/cli_spec.rb
37 | spec/gemirro/configuration_spec.rb
38 | spec/gemirro/gem_spec.rb
39 | spec/gemirro/gem_version_collection_spec.rb
40 | spec/gemirro/gem_version_spec.rb
41 | spec/gemirro/gems_fetcher_spec.rb
42 | spec/gemirro/http_spec.rb
43 | spec/gemirro/indexer_spec.rb
44 | spec/gemirro/mirror_directory_spec.rb
45 | spec/gemirro/mirror_file_spec.rb
46 | spec/gemirro/server_spec.rb
47 | spec/gemirro/source_spec.rb
48 | spec/gemirro/versions_fetcher_spec.rb
49 | spec/gemirro/versions_file_spec.rb
50 | spec/spec_helper.rb
51 | task/manifest.rake
52 | task/rspec.rake
53 | task/rubocop.rake
54 | template/config.rb
55 | template/logs/.gitkeep
56 | template/public/dist/css/gemirro.css
57 | template/public/gems/.gitkeep
58 | views/gem.erb
59 | views/index.erb
60 | views/layout.erb
61 | views/not_found.erb
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gemirro | [](https://travis-ci.org/PierreRambaud/gemirro) [](http://badge.fury.io/rb/gemirro)
2 |
3 | Gemirro is a Ruby application that makes it easy way to create your own RubyGems mirror without having to push or write all gem you wanted in a configuration file.
4 | It does mirroring without any authentication and you can add your private gems in the `gems` directory.
5 | More, to mirroring a source, you only need to start the server, and gems will automaticly be downloaded when needed.
6 |
7 | ## Requirements
8 |
9 | * Ruby 3.0 or newer
10 | * Enough space to store Gems
11 | * A recent version of Rubygems (`gem update --system`)
12 |
13 | ## Installation
14 |
15 | Assuming RubyGems isn't down you can install the Gem as following:
16 |
17 | ```bash
18 | $ gem install gemirro
19 | ```
20 |
21 | ## Usage
22 |
23 | The process of setting up a mirror is fairly easy and can be done in few seconds.
24 |
25 | The first step is to set up a new, empty mirror directory.
26 | This is done by running the `gemirro init` command.
27 |
28 | ```bash
29 | $ gemirro init /srv/http/mirror.com/
30 | ```
31 |
32 | Once created you can edit the main configuration file called [config.rb](https://github.com/PierreRambaud/gemirro/blob/master/template/config.rb).
33 | This configuration file specifies what source to mirror, destination directory, server host and port, etc.
34 |
35 | Once configured and if you add gem in the `define_source`, you can pull them by running the following command:
36 |
37 | ```bash
38 | $ gemirro update
39 | ```
40 |
41 | Once all the Gems have been downloaded you'll need to generate an index of all the installed files. This can be done as following:
42 |
43 | ```bash
44 | $ gemirro index
45 | $ gemirro index --update # Or only update new files
46 | ```
47 |
48 | Last, launch the server, and all requests will check if gems are detected, and download them if necessary and generate index immediately.
49 |
50 | ```bash
51 | $ gemirro server --start
52 | $ gemirro server --status
53 | $ gemirro server --restart
54 | $ gemirro server --stop
55 |
56 | ```
57 |
58 | If you want to use a custom configuration file not located in the current directory, use the `-c` or `--config` option.
59 |
60 | ### Available commands
61 |
62 | ```
63 | Usage: gemirro [COMMAND] [OPTIONS]
64 |
65 | Options:
66 |
67 | -v, --version Shows the current version
68 | -h, --help Display this help message.
69 |
70 | Available commands:
71 |
72 | index Retrieve specs list from source.
73 | init Sets up a new mirror
74 | list List available gems.
75 | server Manage web server
76 | update Updates the list of Gems
77 |
78 | See ` --help` for more information on a specific command.
79 | ```
80 |
81 | ## Apache configuration
82 |
83 | You must activate the apache `proxy` module.
84 |
85 | ```bash
86 | $ sudo a2enmod proxy
87 | $ sudo a2enmod proxy_http
88 | ```
89 |
90 | Create your VirtualHost and replace following `http://localhost:2000` with your custom server configuration located in your `config.rb` file and restart Apache.
91 |
92 | ```
93 |
94 | ServerName mirror.gemirro
95 | ProxyPreserveHost On
96 | ProxyRequests off
97 | ProxyPass / http://localhost:2000/
98 | ProxyPassReverse / http://localhost:2000/
99 |
100 | ```
101 |
102 | ## Nginx configuration
103 |
104 | Replace `localhost:2000` with your custom server configuration located in your `config.rb` file and restart Nginx.
105 |
106 | ```
107 | upstream gemirro {
108 | server localhost:2000;
109 | }
110 |
111 | server {
112 | server_name rbgems;
113 |
114 | location / {
115 | proxy_pass http://gemirro;
116 | proxy_set_header Host $http_host;
117 | proxy_set_header X-Real-IP $remote_addr;
118 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
119 | }
120 | }
121 | ```
122 |
123 | ## Known issues
124 |
125 | ### could not find a temporary directory
126 |
127 | If you use ruby >= 2.0, some urls in the server throwing errors telling `could not find a temporary directory`.
128 | You only need to do a `chmod o+t /tmp`
129 |
130 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 |
3 | GEMSPEC = Gem::Specification.load('gemirro.gemspec')
4 |
5 | Dir['./task/*.rake'].each do |task|
6 | import(task)
7 | end
8 |
9 | task default: %i[spec rubocop]
10 |
--------------------------------------------------------------------------------
/bin/gemirro:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require File.expand_path('../../lib/gemirro', __FILE__)
5 |
6 | options = Gemirro::CLI.options
7 |
8 | begin
9 | puts options if options.parse.empty?
10 | rescue Slop::InvalidOptionError => e
11 | puts e.message
12 | puts options
13 | end
14 |
--------------------------------------------------------------------------------
/gemirro.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'date'
4 | require File.expand_path('../lib/gemirro/version', __FILE__)
5 |
6 | Gem::Specification.new do |s|
7 | s.name = 'gemirro'
8 | s.version = Gemirro::VERSION
9 | s.authors = ['Pierre Rambaud']
10 | s.email = 'pierre.rambaud86@gmail.com'
11 | s.license = 'GPL-3.0'
12 | s.summary = 'Gem for easily creating your own gems mirror.'
13 | s.homepage = 'https://github.com/PierreRambaud/gemirro'
14 | s.description = 'Create your own gems mirror.'
15 | s.executables = ['gemirro']
16 |
17 | s.files = File.read(File.expand_path('../MANIFEST', __FILE__)).split("\n")
18 |
19 | s.required_ruby_version = '>= 3.0'
20 |
21 | s.add_dependency 'addressable', '~>2.8'
22 | s.add_dependency 'builder', '~>3.2'
23 | s.add_dependency 'compact_index', '~> 0.15'
24 | s.add_dependency 'confstruct', '~>1.1'
25 | s.add_dependency 'erubis', '~>2.7'
26 | s.add_dependency 'httpclient', '~>2.8'
27 | s.add_dependency 'logger', '> 0' # stdlib
28 | s.add_dependency 'mutex_m', '> 0' # stdlib
29 | s.add_dependency 'ostruct', '> 0' # stdlib
30 | s.add_dependency 'parallel', '~>1.21'
31 | s.add_dependency 'rubygems-generate_index', '> 0' # stdlib
32 | s.add_dependency 'sinatra', '>=3.1', '<4.0'
33 | s.add_dependency 'slop', '~>3.6'
34 | s.add_dependency 'stringio', '> 0' # stdlib
35 | s.add_dependency 'thin', '~>1.8'
36 |
37 | s.metadata['rubygems_mfa_required'] = 'true'
38 | end
39 |
--------------------------------------------------------------------------------
/lib/gemirro.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'builder'
4 | require 'confstruct'
5 | require 'digest/sha2'
6 | require 'fileutils'
7 | require 'httpclient'
8 | require 'json'
9 | require 'logger'
10 | require 'parallel'
11 | require 'rubygems'
12 | require 'rubygems/indexer'
13 | require 'rubygems/user_interaction'
14 | require 'slop'
15 | require 'stringio'
16 | require 'tempfile'
17 | require 'zlib'
18 |
19 | $LOAD_PATH.unshift(File.expand_path('../', __FILE__)) unless $LOAD_PATH.include?(File.expand_path('../', __FILE__))
20 |
21 | require 'gemirro/version'
22 | require 'gemirro/configuration'
23 | require 'gemirro/utils'
24 | require 'gemirro/gem'
25 | require 'gemirro/gem_version'
26 | require 'gemirro/gem_version_collection'
27 | require 'gemirro/http'
28 | require 'gemirro/indexer'
29 | require 'gemirro/source'
30 | require 'gemirro/mirror_directory'
31 | require 'gemirro/mirror_file'
32 | require 'gemirro/versions_file'
33 | require 'gemirro/versions_fetcher'
34 | require 'gemirro/gems_fetcher'
35 |
36 | require 'gemirro/cli'
37 | require 'gemirro/cli/index'
38 | require 'gemirro/cli/init'
39 | require 'gemirro/cli/list'
40 | require 'gemirro/cli/server'
41 | require 'gemirro/cli/update'
42 |
--------------------------------------------------------------------------------
/lib/gemirro/cli.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require File.expand_path('../version', __FILE__)
4 |
5 | module Gemirro
6 | # CLI mode
7 | module CLI
8 | ##
9 | # Hash containing the default Slop options.
10 | #
11 | # @return [Hash]
12 | #
13 | SLOP_OPTIONS = {
14 | strict: true,
15 | help: true,
16 | banner: 'Usage: gemirro [COMMAND] [OPTIONS]'
17 | }.freeze
18 |
19 | ##
20 | # @return [Slop]
21 | #
22 | def self.options
23 | @options ||= default_options
24 | end
25 |
26 | ##
27 | # Loads the specified configuration file or displays an error if it doesn't
28 | # exist.
29 | #
30 | # @param [String] config_file
31 | # @return [Gemirro::Configuration]
32 | #
33 | def self.load_configuration(config_file)
34 | config_file ||= Configuration.default_configuration_file
35 | config_file = File.expand_path(config_file, Dir.pwd)
36 | config_file += '/config.rb' unless config_file.end_with?('.rb') ||
37 | !File.directory?(config_file)
38 |
39 | abort "The configuration file #{config_file} does not exist" unless File.file?(config_file)
40 |
41 | require(config_file)
42 | end
43 |
44 | ##
45 | # @return [Slop]
46 | #
47 | def self.default_options
48 | Slop.new(SLOP_OPTIONS.dup) do
49 | separator "\nOptions:\n"
50 |
51 | on :v, :version, 'Shows the current version' do
52 | puts CLI.version_information
53 | end
54 | end
55 | end
56 |
57 | ##
58 | # Returns a String containing some platform/version related information.
59 | #
60 | # @return [String]
61 | #
62 | def self.version_information
63 | "gemirro v#{Gemirro::VERSION} on #{RUBY_DESCRIPTION}"
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/gemirro/cli/index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gemirro::CLI.options.command 'index' do
4 | banner 'Usage: gemirro index [OPTIONS]'
5 | description 'Retrieve specs list from source.'
6 | separator "\nOptions:\n"
7 |
8 | on :c=, :config=, 'Path to the configuration file'
9 | on :l=, :log_level=, 'Set logger level'
10 | on :u, :update, 'Update only'
11 |
12 | run do |opts, _args|
13 | Gemirro::CLI.load_configuration(opts[:c])
14 | config = Gemirro.configuration
15 | config.logger_level = opts[:l] if opts[:l]
16 |
17 | unless File.directory?(config.destination)
18 | config.logger.error("The directory #{config.destination} does not exist")
19 | abort
20 | end
21 |
22 | indexer = Gemirro::Indexer.new
23 | indexer.ui = Gem::SilentUI.new
24 |
25 | if File.exist?(File.join(config.versions_file))
26 | indexer.download_source_versions
27 | if opts[:u]
28 | config.logger.info('Generating index updates')
29 | indexer.update_index
30 | else
31 | config.logger.info('Generating indexes')
32 | indexer.generate_index
33 | end
34 | else
35 | config.logger.error("#{File.basename(config.versions_file)} file is missing.")
36 | config.logger.error('Run "gemirro update" before running index.')
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/gemirro/cli/init.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gemirro::CLI.options.command 'init' do
4 | banner 'Usage: gemirro init [DIRECTORY] [OPTIONS]'
5 | description 'Sets up a new mirror'
6 | separator "\nOptions:\n"
7 |
8 | on :force, 'Force overwrite'
9 |
10 | run do |opts, args|
11 | directory = File.expand_path(args[0] || Dir.pwd)
12 | template = Gemirro::Configuration.template_directory
13 |
14 | Dir.mkdir(directory) unless File.directory?(directory)
15 |
16 | if opts[:force]
17 | FileUtils.cp_r(File.join(template, '.'), directory)
18 | else
19 | Dir.glob("#{template}/**/*", File::FNM_DOTMATCH).each do |file|
20 | next if ['.', '..'].include?(File.basename(file))
21 |
22 | dest = File.join(directory, file.gsub(/^#{template}/, ''))
23 | next if File.exist?(dest) && dest !~ /gemirro.css/
24 |
25 | FileUtils.cp_r(file, dest)
26 | end
27 | end
28 |
29 | puts "Initialized empty mirror in #{directory}"
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/gemirro/cli/list.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gemirro::CLI.options.command 'list' do
4 | banner 'Usage: gemirro list [OPTIONS]'
5 | description 'List available gems.'
6 | separator "\nOptions:\n"
7 |
8 | on :c=, :config=, 'Path to the configuration file'
9 | on :l=, :log_level=, 'Set logger level'
10 |
11 | run do |opts, _args|
12 | Gemirro::CLI.load_configuration(opts[:c])
13 | config = Gemirro.configuration
14 | config.logger_level = opts[:l] if opts[:l]
15 |
16 | unless File.directory?(config.destination)
17 | config.logger.error("The directory #{config.destination} does not exist")
18 | abort
19 | end
20 |
21 | gems = Gemirro::Utils.gems_collection.group_by(&:name).sort
22 | gems.each do |name, versions|
23 | puts "#{name}: (#{versions.map(&:number).join(', ')})"
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/gemirro/cli/server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gemirro::CLI.options.command 'server' do
4 | banner 'Usage: gemirro server [OPTIONS]'
5 | description 'Manage web server'
6 | separator "\nOptions:\n"
7 |
8 | on :start, 'Run web server'
9 | on :stop, 'Stop web server'
10 | on :restart, 'Restart web server'
11 | on :status, 'Status of web server'
12 | on :c=, :config=, 'Path to the configuration file'
13 | on :l=, :log_level=, 'Set logger level'
14 |
15 | @pid_file = nil
16 |
17 | run do |opts, _args|
18 | load_configuration(opts)
19 | start if opts[:start]
20 | stop if opts[:stop]
21 | restart if opts[:restart]
22 | status if opts[:status]
23 | end
24 |
25 | def load_configuration(opts)
26 | Gemirro::CLI.load_configuration(opts[:c])
27 | config = Gemirro.configuration
28 | config.logger_level = opts[:l] if opts[:l]
29 | unless File.directory?(config.destination)
30 | config.logger.error("The directory #{config.destination} does not exist")
31 | abort
32 | end
33 |
34 | @pid_file = File.expand_path(File.join(config.destination,
35 | '..',
36 | 'gemirro.pid'))
37 | require 'gemirro/server'
38 | end
39 |
40 | # Copy stdout because we'll need to reopen it later on
41 | @orig_stdout = $stdout.clone
42 | $PROGRAM_NAME = 'gemirro'
43 |
44 | def create_pid
45 | File.write(@pid_file, Process.pid.to_s)
46 | rescue Errno::EACCES
47 | $stdout.reopen @orig_stdout
48 | puts "Error: Can't write to #{@pid_file} - Permission denied"
49 | exit!
50 | end
51 |
52 | def destroy_pid
53 | File.delete(@pid_file) if File.exist?(@pid_file) && pid == Process.pid
54 | end
55 |
56 | def pid
57 | File.open(@pid_file, 'r') do |f|
58 | return f.gets.to_i
59 | end
60 | rescue Errno::ENOENT
61 | puts "Error: PID File not found #{@pid_file}"
62 | end
63 |
64 | def start
65 | puts 'Starting...'
66 | if File.exist?(@pid_file) && running?(pid)
67 | puts "Error: #{$PROGRAM_NAME} already running"
68 | abort
69 | end
70 |
71 | Process.daemon if Gemirro::Utils.configuration.server.daemonize
72 | create_pid
73 | $stdout.reopen @orig_stdout
74 | puts "done! (PID is #{pid})\n"
75 | Gemirro::Server.run!
76 | destroy_pid
77 | $stdout.reopen File::NULL, 'a'
78 | end
79 |
80 | def stop
81 | process_pid = pid
82 | return if process_pid.nil?
83 |
84 | begin
85 | Process.kill('TERM', process_pid)
86 | Timeout.timeout(30) { sleep 0.1 while running?(process_pid) }
87 | rescue Errno::ESRCH
88 | puts "Error: Couldn't find process with PID #{process_pid}"
89 | exit!
90 | rescue Timeout::Error
91 | puts 'timeout while sending TERM signal, sending KILL signal now... '
92 | Process.kill('KILL', process_pid)
93 | destroy_pid
94 | end
95 | puts 'done!'
96 | end
97 |
98 | def restart
99 | stop
100 | start
101 | end
102 |
103 | def status
104 | if running?(pid)
105 | puts "#{$PROGRAM_NAME} is running"
106 | else
107 | puts "#{$PROGRAM_NAME} is not running"
108 | abort
109 | end
110 | end
111 |
112 | def running?(process_id)
113 | return false if process_id.nil?
114 |
115 | Process.getpgid(process_id.to_i) != -1
116 | rescue Errno::ESRCH
117 | false
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/lib/gemirro/cli/update.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gemirro::CLI.options.command 'update' do
4 | banner 'Usage: gemirro update [OPTIONS]'
5 | description 'Updates the list of Gems'
6 | separator "\nOptions:\n"
7 |
8 | on :c=, :config=, 'Path to the configuration file'
9 | on :l=, :log_level=, 'Set logger level'
10 |
11 | run do |opts, _args|
12 | Gemirro::CLI.load_configuration(opts[:c])
13 | config.logger_level = opts[:l] if opts[:l]
14 |
15 | source = Gemirro.configuration.source
16 | versions = Gemirro::VersionsFetcher.new(source).fetch
17 | gems = Gemirro::GemsFetcher.new(source, versions)
18 |
19 | gems.fetch
20 |
21 | source.gems.each do |gem|
22 | gem.gemspec = true
23 | end
24 |
25 | gems.fetch
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/gemirro/configuration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Configuration
4 | module Gemirro
5 | ##
6 | # @return [Gemirro::Configuration]
7 | #
8 | def self.configuration
9 | default_config = {
10 | server: {
11 | access_log: '/tmp/gemirro.access.log',
12 | error_log: '/tmp/gemirro.access.log',
13 | daemonize: true
14 | },
15 | update_thread_count: begin; Etc.nprocessors - 1; rescue StandardError; 4; end,
16 | update_on_fetch: true,
17 | fetch_gem: true
18 | }
19 | @configuration ||= Configuration.new(default_config)
20 | end
21 |
22 | ##
23 | # Configuration class used for storing data about a mirror such as the
24 | # destination directory, source, ignored Gems, etc.
25 | #
26 | class Configuration < Confstruct::Configuration
27 | attr_accessor :source
28 | attr_writer :logger
29 |
30 | LOGGER_LEVEL = {
31 | 'debug' => Logger::DEBUG,
32 | 'warning' => Logger::WARN,
33 | 'info' => Logger::INFO,
34 | 'unknown' => Logger::UNKNOWN,
35 | 'error' => Logger::ERROR,
36 | 'fatal' => Logger::FATAL
37 | }.freeze
38 |
39 | ##
40 | # Returns the logger
41 | #
42 | # @return [Logger]
43 | #
44 | def logger
45 | @logger ||= Logger.new($stdout)
46 | end
47 |
48 | ##
49 | # Set log level
50 | #
51 | # @param [string]
52 | #
53 | # @return [Logger]
54 | #
55 | def logger_level=(level)
56 | logger.level = LOGGER_LEVEL[level] if LOGGER_LEVEL.key?(level)
57 | logger
58 | end
59 |
60 | ##
61 | # Returns the template path to init directory
62 | #
63 | # @return [String]
64 | #
65 | def self.template_directory
66 | File.expand_path('../../../template', __FILE__)
67 | end
68 |
69 | ##
70 | # Returns the views path to render templates
71 | #
72 | # @return [String]
73 | #
74 | def self.views_directory
75 | File.expand_path('../../../views', __FILE__)
76 | end
77 |
78 | ##
79 | # Returns default configuration file path
80 | #
81 | # @return [String]
82 | #
83 | def self.default_configuration_file
84 | File.expand_path('config.rb', Dir.pwd)
85 | end
86 |
87 | ##
88 | # Returns the name of the directory that contains the quick
89 | # specification files.
90 | #
91 | # @return [String]
92 | #
93 | def self.marshal_identifier
94 | "Marshal.#{marshal_version}"
95 | end
96 |
97 | ##
98 | # Returns the name of the file that contains an index of all the versions.
99 | #
100 | # @return [String]
101 | #
102 | def versions_file
103 | return unless @source
104 |
105 | File.expand_path("#{URI.parse(@source.host).host.gsub('.', '_')}_versions", destination.to_s)
106 | end
107 |
108 | ##
109 | # Returns a String containing the Marshal version.
110 | #
111 | # @return [String]
112 | #
113 | def self.marshal_version
114 | "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}"
115 | end
116 |
117 | ##
118 | # Return mirror directory
119 | #
120 | # @return [Gemirro::MirrorDirectory]
121 | #
122 | def mirror_gems_directory
123 | @mirror_gems_directory ||= MirrorDirectory.new(gems_directory)
124 | end
125 |
126 | ##
127 | # Returns gems directory
128 | #
129 | # @return [String]
130 | #
131 | def gems_directory
132 | File.join(destination.to_s, 'gems')
133 | end
134 |
135 | ##
136 | # Return mirror directory
137 | #
138 | # @return [Gemirro::MirrorDirectory]
139 | #
140 | def mirror_gemspecs_directory
141 | @mirror_gemspecs_directory ||= MirrorDirectory.new(gemspecs_directory)
142 | end
143 |
144 | ##
145 | # Returns gems directory
146 | #
147 | # @return [String]
148 | #
149 | def gemspecs_directory
150 | File.join(destination.to_s, 'quick', self.class.marshal_identifier)
151 | end
152 |
153 | ##
154 | # Returns a Hash containing various Gems to ignore and their versions.
155 | #
156 | # @return [Hash]
157 | #
158 | def ignored_gems
159 | @ignored_gems ||= Hash.new { |hash, key| hash[key] = {} }
160 | end
161 |
162 | ##
163 | # Adds a Gem to the list of Gems to ignore.
164 | #
165 | # @param [String] name
166 | # @param [String] version
167 | #
168 | def ignore_gem(name, version, platform)
169 | ignored_gems[platform] ||= {}
170 | ignored_gems[platform][name] ||= []
171 | ignored_gems[platform][name] << version
172 | end
173 |
174 | ##
175 | # Checks if a Gem should be ignored.
176 | #
177 | # @param [String] name
178 | # @param [String] version
179 | # @return [TrueClass|FalseClass]
180 | #
181 | def ignore_gem?(name, version, platform)
182 | if ignored_gems[platform][name]
183 | ignored_gems[platform][name].include?(version)
184 | else
185 | false
186 | end
187 | end
188 |
189 | ##
190 | # Define the source to mirror.
191 | #
192 | # @param [String] name
193 | # @param [String] url
194 | # @param [Proc] block
195 | #
196 | def define_source(name, url, &block)
197 | source = Source.new(name, url)
198 | source.instance_eval(&block)
199 |
200 | @source = source
201 | end
202 | end
203 | end
204 |
--------------------------------------------------------------------------------
/lib/gemirro/gem.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The Gem class contains data about a Gem such as the name, requirement as
6 | # well as providing some methods to more easily extract the specific version
7 | # number.
8 | #
9 | # @!attribute [r] name
10 | # @return [String]
11 | # @!attribute [r] requirement
12 | # @return [Gem::Requirement]
13 | # @!attribute [r] version
14 | # @return [Gem::Version]
15 | #
16 | class Gem
17 | attr_reader :name, :requirement
18 | attr_accessor :gemspec, :platform
19 |
20 | ONLY_LATEST = %i[latest newest].freeze
21 |
22 | ##
23 | # Returns a `Gem::Version` instance based on the specified requirement.
24 | #
25 | # @param [Gem::Requirement] requirement
26 | # @return [Gem::Version]
27 | #
28 | def self.version_for(requirement)
29 | ::Gem::Version.new(requirement.requirements.max.last.version)
30 | end
31 |
32 | ##
33 | # @param [String] name
34 | # @param [Gem::Requirement|String] requirement
35 | #
36 | def initialize(name, requirement = nil, platform = 'ruby')
37 | requirement ||= ::Gem::Requirement.default
38 |
39 | requirement = ::Gem::Requirement.new(requirement) if requirement.is_a?(String)
40 |
41 | @name = name
42 | @requirement = requirement
43 | @platform = platform
44 | end
45 |
46 | ##
47 | # Returns the version
48 | #
49 | # @return [Gem::Version]
50 | #
51 | def version
52 | @version ||= self.class.version_for(requirement)
53 | end
54 |
55 | ##
56 | # Define if version exists
57 | #
58 | # @return [TrueClass|FalseClass]
59 | #
60 | def version?
61 | version && !version.segments.reject(&:zero?).empty?
62 | end
63 |
64 | ##
65 | # Define if version exists
66 | #
67 | # @return [TrueClass|FalseClass]
68 | #
69 | def only_latest?
70 | @requirement.is_a?(Symbol) && ONLY_LATEST.include?(@requirement)
71 | end
72 |
73 | ##
74 | # Is gemspec
75 | #
76 | # @return [TrueClass|FalseClass]
77 | #
78 | def gemspec?
79 | @gemspec == true
80 | end
81 |
82 | ##
83 | # Returns the filename of the gem file.
84 | #
85 | # @param [String] gem_version
86 | # @return [String]
87 | #
88 | def filename(gem_version = nil)
89 | gem_version ||= version.to_s
90 | n = [name, gem_version]
91 | n.push(@platform) if @platform != 'ruby'
92 | "#{n.join('-')}.gem"
93 | end
94 |
95 | ##
96 | # Returns the filename of the gemspec file.
97 | #
98 | # @param [String] gem_version
99 | # @return [String]
100 | #
101 | def gemspec_filename(gem_version = nil)
102 | gem_version ||= version.to_s
103 | n = [name, gem_version]
104 | n.push(@platform) if @platform != 'ruby'
105 | "#{n.join('-')}.gemspec.rz"
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/lib/gemirro/gem_version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The Gem class contains data about a Gem such as the name, requirement as
6 | # well as providing some methods to more easily extract the specific version
7 | # number.
8 | #
9 | # @!attribute [r] name
10 | # @return [String]
11 | # @!attribute [r] number
12 | # @return [Integer]
13 | # @!attribute [r] platform
14 | # @return [String]
15 | # @!attribute [r] version
16 | # @return [Gem::Version]
17 | #
18 | class GemVersion
19 | include Comparable
20 | attr_reader :name, :number, :platform
21 |
22 | ##
23 | # @param [String] name
24 | # @param [String] number
25 | # @param [String] platform
26 | #
27 | def initialize(name, number, platform)
28 | @name = name
29 | @number = number
30 | @platform = platform
31 | end
32 |
33 | ##
34 | # Is for ruby
35 | #
36 | # @return [Boolean]
37 | #
38 | def ruby?
39 | !(@platform =~ /^ruby$/i).nil?
40 | end
41 |
42 | ##
43 | # Retrieve gem version
44 | #
45 | # @return [Gem::Version]
46 | #
47 | def version
48 | @version ||= ::Gem::Version.create(number)
49 | end
50 |
51 | ##
52 | # Compare gem to another
53 | #
54 | # @return [Integer]
55 | #
56 | def <=>(other)
57 | sort = other.name <=> @name
58 | sort = version <=> other.version if sort.zero?
59 | sort = other.ruby? && !ruby? ? 1 : -1 if sort.zero? &&
60 | ruby? != other.ruby?
61 | sort = other.platform <=> @platform if sort.zero?
62 |
63 | sort
64 | end
65 |
66 | ##
67 | # Gemfile name
68 | #
69 | # @return [String]
70 | #
71 | def gemfile_name
72 | platform = ruby? ? nil : @platform
73 | [@name, @number, platform].compact.join('-')
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/lib/gemirro/gem_version_collection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The VersionCollection class contains a collection of ::Gem::Version
6 | #
7 | # @!attribute [r] gems
8 | # @return [Array]
9 | # @!attribute [r] grouped
10 | # @return [Array]
11 | #
12 | class GemVersionCollection
13 | include Enumerable
14 |
15 | attr_reader :gems, :grouped
16 |
17 | ##
18 | # @param [Array] gems
19 | #
20 | def initialize(gems = [])
21 | @gems = gems.map do |object|
22 | if object.is_a?(GemVersion)
23 | object
24 | else
25 | GemVersion.new(*object)
26 | end
27 | end
28 |
29 | @gems.sort_by!(&:version)
30 | end
31 |
32 | ##
33 | # Return oldest version of a gem
34 | #
35 | # @return [GemVersion]
36 | #
37 | def oldest
38 | @gems.first
39 | end
40 |
41 | ##
42 | # Return newest version of a gem
43 | #
44 | # @return [GemVersion]
45 | #
46 | def newest
47 | @gems.last
48 | end
49 |
50 | ##
51 | # Return size of a gem
52 | #
53 | # @return [Integer]
54 | #
55 | def size
56 | @gems.size
57 | end
58 |
59 | ##
60 | # Each method
61 | #
62 | def each(&block)
63 | @gems.each(&block)
64 | end
65 |
66 | ##
67 | # Group gems by name
68 | #
69 | # @param [Proc] block
70 | # @return [Array]
71 | #
72 | def by_name(&block)
73 | if @grouped.nil?
74 | @grouped = @gems.group_by(&:name).map do |name, collection|
75 | [name, GemVersionCollection.new(collection)]
76 | end
77 |
78 | @grouped.reject! do |name, _collection|
79 | name.nil?
80 | end
81 |
82 | @grouped.sort_by! do |name, _collection|
83 | name.downcase
84 | end
85 | end
86 |
87 | if block_given?
88 | @grouped.each(&block)
89 | else
90 | @grouped
91 | end
92 | end
93 |
94 | ##
95 | # Find gem by name
96 | #
97 | # @param [String] gemname
98 | # @return [Array]
99 | #
100 | def find_by_name(gemname)
101 | gem = by_name.select do |name, _collection|
102 | name == gemname
103 | end
104 |
105 | gem.first.last if gem.any?
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/lib/gemirro/gems_fetcher.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The GemsFetcher class is responsible for downloading Gems from an external
6 | # source.
7 | #
8 | # @!attribute [r] source
9 | # @return [Source]
10 | # @!attribute [r] versions_file
11 | # @return [Gemirro::VersionsFile]
12 | #
13 | class GemsFetcher
14 | attr_reader :source, :versions_file
15 |
16 | ##
17 | # @param [Source] source
18 | # @param [Gemirro::VersionsFile] versions_file
19 | #
20 | def initialize(source, versions_file)
21 | @source = source
22 | @versions_file = versions_file
23 | end
24 |
25 | ##
26 | # Fetches the Gems.
27 | #
28 | def fetch
29 | @source.gems.each do |gem|
30 | versions_for(gem).each do |versions|
31 | gem.platform = versions[1] if versions
32 | version = versions[0] if versions
33 | if gem.gemspec?
34 | gemspec = fetch_gemspec(gem, version)
35 | if gemspec
36 | Utils.configuration.mirror_gemspecs_directory
37 | .add_file(gem.gemspec_filename(version), gemspec)
38 | end
39 | else
40 | gemfile = fetch_gem(gem, version)
41 | if gemfile
42 | Utils.configuration.mirror_gems_directory
43 | .add_file(gem.filename(version), gemfile)
44 | end
45 | end
46 | end
47 | end
48 | end
49 |
50 | ##
51 | # Returns an Array containing the versions that should be fetched for a
52 | # Gem.
53 | #
54 | # @param [Gemirro::Gem] gem
55 | # @return [Array]
56 | #
57 | def versions_for(gem)
58 | return [] unless @versions_file
59 |
60 | available = @versions_file.versions_for(gem.name)
61 | return [available.last] if gem.only_latest?
62 |
63 | versions = available.select do |v|
64 | gem.requirement.satisfied_by?(v[0])
65 | end
66 |
67 | versions = [available.last] if versions.empty?
68 |
69 | versions
70 | end
71 |
72 | ##
73 | # Tries to download gemspec from a given name and version
74 | #
75 | # @param [Gemirro::Gem] gem
76 | # @param [Gem::Version] version
77 | # @return [String]
78 | #
79 | def fetch_gemspec(gem, version)
80 | filename = gem.gemspec_filename(version)
81 | satisfied = if gem.only_latest?
82 | true
83 | else
84 | gem.requirement.satisfied_by?(version)
85 | end
86 |
87 | if gemspec_exists?(filename) || !satisfied
88 | Utils.logger.debug("Skipping #{filename}")
89 | return
90 | end
91 |
92 | Utils.logger.info("Fetching #{filename}")
93 | fetch_from_source(filename, gem, version, true)
94 | end
95 |
96 | ##
97 | # Tries to download the gem file from a given nam and version
98 | #
99 | # @param [Gemirro::Gem] gem
100 | # @param [Gem::Version] version
101 | # @return [String]
102 | #
103 | def fetch_gem(gem, version)
104 | filename = gem.filename(version)
105 | satisfied = gem.only_latest? || gem.requirement.satisfied_by?(version)
106 | name = gem.name
107 |
108 | if gem_exists?(filename) || ignore_gem?(name, version, gem.platform) || !satisfied
109 | Utils.logger.debug("Skipping #{filename}")
110 | return
111 | end
112 |
113 | Utils.configuration.ignore_gem(gem.name, version, gem.platform)
114 | Utils.logger.info("Fetching #{filename}")
115 |
116 | fetch_from_source(filename, gem, version)
117 | end
118 |
119 | ##
120 | #
121 | # @param [String] filename
122 | # @param [Gemirro::Gem] gem
123 | # @param [Gem::Version] version
124 | # @return [String]
125 | #
126 | def fetch_from_source(filename, gem, version, gemspec = false)
127 | data = nil
128 | begin
129 | data = @source.fetch_gem(filename) unless gemspec
130 | data = @source.fetch_gemspec(filename) if gemspec
131 | rescue StandardError => e
132 | filename = gem.filename(version)
133 | Utils.logger.error("Failed to retrieve #{filename}: #{e.message}")
134 | Utils.logger.debug("Adding #{filename} to the list of ignored Gems")
135 |
136 | Utils.configuration.ignore_gem(gem.name, version, gem.platform)
137 | end
138 |
139 | data
140 | end
141 |
142 | ##
143 | # Checks if a given Gem has already been downloaded.
144 | #
145 | # @param [String] filename
146 | # @return [TrueClass|FalseClass]
147 | #
148 | def gem_exists?(filename)
149 | Utils.configuration.mirror_gems_directory.file_exists?(filename)
150 | end
151 |
152 | ##
153 | # Checks if a given Gemspec has already been downloaded.
154 | #
155 | # @param [String] filename
156 | # @return [TrueClass|FalseClass]
157 | #
158 | def gemspec_exists?(filename)
159 | Utils.configuration.mirror_gemspecs_directory.file_exists?(filename)
160 | end
161 |
162 | ##
163 | # @see Gemirro::Configuration#ignore_gem?
164 | #
165 | def ignore_gem?(*args)
166 | Utils.configuration.ignore_gem?(*args)
167 | end
168 | end
169 | end
170 |
--------------------------------------------------------------------------------
/lib/gemirro/http.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The Http class is responsible for executing GET request
6 | # to a specific url and return an response as an HTTP::Message
7 | #
8 | # @!attribute [r] client
9 | # @return [HTTPClient]
10 | #
11 | class Http
12 | attr_accessor :client
13 |
14 | ##
15 | # Requests the given HTTP resource.
16 | #
17 | # @param [String] url
18 | # @return [HTTP::Message]
19 | #
20 | def self.get(url)
21 | response = client.get(url, follow_redirect: true)
22 |
23 | raise HTTPClient::BadResponseError, response.reason unless HTTP::Status.successful?(response.status)
24 |
25 | response
26 | end
27 |
28 | ##
29 | # @return [HTTPClient]
30 | #
31 | def self.client
32 | client ||= HTTPClient.new
33 | config = Utils.configuration
34 | if defined?(config.upstream_user)
35 | user = config.upstream_user
36 | password = config.upstream_password
37 | domain = config.upstream_domain
38 | client.set_auth(domain, user, password)
39 | end
40 |
41 | if defined?(config.proxy)
42 | proxy = config.proxy
43 | client.proxy = (proxy)
44 | end
45 |
46 | # Use my own ca file for self signed cert
47 | if defined?(config.rootca)
48 | abort "The configuration file #{config.rootca} does not exist" unless File.file?(config.rootca)
49 | client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_PEER
50 | client.ssl_config.set_trust_ca(config.rootca)
51 | elsif defined?(config.verify_mode)
52 | client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE unless config.verify_mode
53 | end
54 |
55 | # Enforce base auth
56 | if defined?(config.basic_auth) && config.basic_auth
57 | client.www_auth.basic_auth.force_auth = (true)
58 | end
59 | @client = client
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/gemirro/indexer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The Indexer class is responsible for downloading useful file directly
6 | # on the source host, such as specs-*.*.gz, marshal information, etc...
7 | #
8 | # @!attribute [r] files
9 | # @return [Array]
10 | # @!attribute [r] quick_marshal_dir
11 | # @return [String]
12 | # @!attribute [r] directory
13 | # @return [String]
14 | # @!attribute [r] dest_directory
15 | # @return [String]
16 | # @!attribute [r] only_origin
17 | # @return [Boolean]
18 | # @!attribute [r] updated_gems
19 | # @return [Array]
20 | #
21 | class Indexer < ::Gem::Indexer
22 | attr_accessor(
23 | :files,
24 | :quick_marshal_dir,
25 | :directory,
26 | :dest_directory,
27 | :only_origin,
28 | :updated_gems
29 | )
30 |
31 | ##
32 | # Create an indexer that will index the gems in +directory+.
33 | #
34 | # @param [String] directory Destination directory
35 | # @param [Hash] options Indexer options
36 | # @return [Array]
37 | ##
38 | def initialize(options = {})
39 | require 'fileutils'
40 | require 'tmpdir'
41 | require 'zlib'
42 | require 'builder/xchar'
43 | require 'compact_index'
44 |
45 | options.merge!({ build_modern: false })
46 |
47 | @dest_directory = Gemirro.configuration.destination
48 | @directory =
49 | File.join(Dir.tmpdir, "gem_generate_index_#{rand(1_000_000_000)}")
50 |
51 | marshal_name = "Marshal.#{::Gem.marshal_version}"
52 |
53 | @master_index =
54 | File.join(@directory, 'yaml')
55 | @marshal_index =
56 | File.join(@directory, marshal_name)
57 |
58 | @quick_dir = File.join(@directory, 'quick')
59 | @quick_marshal_dir =
60 | File.join(@quick_dir, marshal_name)
61 | @quick_marshal_dir_base =
62 | File.join(@dest_directory, 'quick', marshal_name) # FIX: UGH
63 |
64 | @quick_index =
65 | File.join(@quick_dir, 'index')
66 | @latest_index =
67 | File.join(@quick_dir, 'latest_index')
68 |
69 | @latest_specs_index =
70 | File.join(@directory, "latest_specs.#{::Gem.marshal_version}")
71 | @dest_latest_specs_index =
72 | File.join(@dest_directory, "latest_specs.#{::Gem.marshal_version}")
73 | @infos_dir =
74 | File.join(@dest_directory, 'info')
75 |
76 | @files = []
77 | end
78 |
79 | ##
80 | # Generate indices on the destination directory
81 | #
82 | # @return [Array]
83 | #
84 | def install_indices
85 | Utils.logger
86 | .debug("Downloading index into production dir #{@dest_directory}")
87 |
88 | files = @files
89 | files.delete @quick_marshal_dir if files.include? @quick_dir
90 |
91 | if files.include?(@quick_marshal_dir) && !files.include?(@quick_dir)
92 | files.delete @quick_marshal_dir
93 | FileUtils.mkdir_p(File.dirname(@quick_marshal_dir_base), verbose: verbose)
94 | if @quick_marshal_dir_base && File.exist?(@quick_marshal_dir_base)
95 | FileUtils.rm_rf(@quick_marshal_dir_base, verbose: verbose)
96 | end
97 | FileUtils.mv(@quick_marshal_dir, @quick_marshal_dir_base, verbose: verbose, force: true)
98 | end
99 |
100 | files.each do |path|
101 | file = path.sub(%r{^#{Regexp.escape @directory}/?}, '')
102 |
103 | source_content = download_from_source(file)
104 | next if source_content.nil?
105 |
106 | MirrorFile.new(File.join(@dest_directory, file)).write(source_content)
107 |
108 | FileUtils.rm_rf(path)
109 | end
110 | end
111 |
112 | ##
113 | # Download file from source (example: rubygems.org)
114 | #
115 | # @param [String] file File path
116 | # @return [String]
117 | #
118 | def download_from_source(file)
119 | source_host = Gemirro.configuration.source.host
120 | Utils.logger.info("Download from source #{source_host}/#{file}")
121 | resp = Http.get("#{source_host}/#{File.basename(file)}")
122 | return unless resp.code == 200
123 |
124 | resp.body
125 | end
126 |
127 | ##
128 | # Build indices
129 | #
130 | # @return [Array]
131 | #
132 | def build_indices
133 | specs = *map_gems_to_specs(gem_file_list)
134 | specs.select! { |s| s.instance_of?(::Gem::Specification) }
135 | ::Gem::Specification.dirs = []
136 | ::Gem::Specification.all = specs
137 |
138 | build_marshal_gemspecs(specs)
139 |
140 | build_compact_index_names
141 | build_compact_index_infos(specs)
142 | build_compact_index_versions(specs)
143 | end
144 |
145 | ##
146 | # Cache compact_index endpoint /names
147 | # Report all gems with versions available. Does not require opening spec files.
148 | #
149 | # @return nil
150 | #
151 | def build_compact_index_names
152 | Utils.logger.info('[1/1]: Caching /names')
153 | FileUtils.rm_rf(Dir.glob(File.join(@dest_directory, 'names*.list')))
154 |
155 | gem_name_list = Dir.glob('*.gem', base: File.join(@dest_directory, 'gems')).collect do |x|
156 | x.sub(/-\d+(\.\d+)*(\.[a-zA-Z\d]+)*([-_a-zA-Z\d]+)?\.gem/, '')
157 | end.uniq.sort!
158 |
159 | Tempfile.create('names.list') do |f|
160 | f.write CompactIndex.names(gem_name_list).to_s
161 | f.rewind
162 | FileUtils.cp(
163 | f.path,
164 | File.join(
165 | @dest_directory,
166 | "names.#{Digest::MD5.file(f.path).hexdigest}.#{Digest::SHA256.file(f.path).hexdigest}.list"
167 | ),
168 | verbose: verbose
169 | )
170 | end
171 |
172 | nil
173 | end
174 |
175 | ##
176 | # Cache compact_index endpoint /versions
177 | #
178 | # @param [Array] specs Gems list
179 | # @param [Boolean] partial Is gem list an update or a full index
180 | # @return nil
181 | #
182 | def build_compact_index_versions(specs, partial = false)
183 | Utils.logger.info('[1/1]: Caching /versions')
184 |
185 | cg =
186 | specs
187 | .sort_by(&:name)
188 | .group_by(&:name)
189 | .collect do |name, gem_versions|
190 | gem_versions =
191 | gem_versions.sort do |a, b|
192 | a.version <=> b.version
193 | end
194 |
195 | info_file = Dir.glob(File.join(@infos_dir, "#{name}.*.*.list")).last
196 |
197 | throw "Info file for #{name} not found" unless info_file
198 |
199 | info_file_checksum = info_file.split('.', -4)[-3]
200 |
201 | CompactIndex::Gem.new(
202 | name,
203 | gem_versions.collect do |y|
204 | CompactIndex::GemVersion.new(
205 | y.version.to_s,
206 | y.platform,
207 | nil,
208 | info_file_checksum
209 | )
210 | end
211 | )
212 | end
213 |
214 | Tempfile.create('versions.list') do |f|
215 | previous_versions_file = Dir.glob(File.join(@dest_directory, 'versions.*.*.list')).last
216 |
217 | if partial && previous_versions_file
218 | versions_file = CompactIndex::VersionsFile.new(previous_versions_file)
219 | else
220 | versions_file = CompactIndex::VersionsFile.new(f.path)
221 | f.write format('created_at: %s', Time.now.utc.iso8601)
222 | f.write "\n---\n"
223 | end
224 |
225 | f.write CompactIndex.versions(versions_file, cg)
226 | f.rewind
227 |
228 | FileUtils.rm_rf(Dir.glob(File.join(@dest_directory, 'versions.*.*.list')))
229 |
230 | FileUtils.cp(
231 | f.path,
232 | File.join(
233 | @dest_directory,
234 | "versions.#{Digest::MD5.file(f.path).hexdigest}.#{Digest::SHA256.file(f.path).hexdigest}.list"
235 | ),
236 | verbose: verbose
237 | )
238 | end
239 |
240 | nil
241 | end
242 |
243 | ##
244 | # Cache compact_index endpoint /info/[gemname]
245 | #
246 | # @param [Array] specs Gems list
247 | # @param [Boolean] partial Is gem list an update or a full index
248 | # @return nil
249 | #
250 | def build_compact_index_infos(specs, partial = false)
251 | FileUtils.mkdir_p(@infos_dir, verbose: verbose)
252 |
253 | if partial
254 | specs.collect(&:name).uniq do |name|
255 | FileUtils.rm_rf(Dir.glob(File.join(@infos_dir, "#{name}.*.*.list")))
256 | end
257 | else
258 | FileUtils.rm_rf(Dir.glob(File.join(@infos_dir, '*.list')))
259 | end
260 |
261 | grouped_specs = specs.sort_by(&:name).group_by(&:name)
262 | grouped_specs.each_with_index do |(name, gem_versions), index|
263 | Utils.logger.info("[#{index + 1}/#{grouped_specs.size}]: Caching /info/#{name}")
264 |
265 | gem_versions =
266 | gem_versions.sort do |a, b|
267 | a.version <=> b.version
268 | end
269 |
270 | versions =
271 | Parallel.map(gem_versions, in_threads: Utils.configuration.update_thread_count) do |spec|
272 | deps =
273 | spec
274 | .dependencies
275 | .select { |d| d.type == :runtime }
276 | .sort_by(&:name)
277 | .collect do |dependency|
278 | CompactIndex::Dependency.new(
279 | dependency.name,
280 | dependency.requirement.to_s
281 | )
282 | end
283 |
284 | CompactIndex::GemVersion.new(
285 | spec.version,
286 | spec.platform,
287 | Digest::SHA256.file(spec.loaded_from).hexdigest,
288 | nil,
289 | deps,
290 | spec.required_ruby_version.to_s,
291 | spec.required_rubygems_version.to_s
292 | )
293 | end
294 |
295 | Tempfile.create("info_#{name}.list") do |f|
296 | f.write CompactIndex.info(versions).to_s
297 | f.rewind
298 |
299 | FileUtils.cp(
300 | f.path,
301 | File.join(
302 | @infos_dir,
303 | "#{name}.#{Digest::MD5.file(f.path).hexdigest}.#{Digest::SHA256.file(f.path).hexdigest}.list"
304 | ),
305 | verbose: verbose
306 | )
307 | end
308 | end
309 |
310 | nil
311 | end
312 |
313 | ##
314 | # Map gems file to specs
315 | #
316 | # @param [Array] gems Gems list
317 | # @return [Array]
318 | #
319 | def map_gems_to_specs(gems)
320 | results = []
321 |
322 | Parallel.each_with_index(gems, in_threads: Utils.configuration.update_thread_count) do |gemfile, index|
323 | Utils.logger.info("[#{index + 1}/#{gems.size}]: Processing #{gemfile.split('/')[-1]}")
324 | if File.empty?(gemfile)
325 | Utils.logger.warn("Skipping zero-length gem: #{gemfile}")
326 | next
327 | end
328 |
329 | begin
330 | begin
331 | spec =
332 | if ::Gem::Package.respond_to? :open
333 | ::Gem::Package.open(File.open(gemfile, 'rb'), 'r', &:metadata)
334 | else
335 | ::Gem::Package.new(gemfile).spec
336 | end
337 | rescue NotImplementedError
338 | next
339 | end
340 |
341 | spec.loaded_from = gemfile
342 |
343 | # HACK: fuck this shit - borks all tests that use pl1
344 | if File.basename(gemfile, '.gem') != spec.original_name
345 | exp = spec.full_name
346 | exp << " (#{spec.original_name})" if
347 | spec.original_name != spec.full_name
348 | msg = "Skipping misnamed gem: #{gemfile} should be named #{exp}"
349 | Utils.logger.warn(msg)
350 | next
351 | end
352 |
353 | version = spec.version.version
354 | unless version =~ /^\d+(\.\d+)?(\.\d+)?.*/
355 | msg = "Skipping gem #{spec.full_name} - invalid version #{version}"
356 | Utils.logger.warn(msg)
357 | next
358 | end
359 |
360 | spec.abbreviate
361 | spec.sanitize
362 |
363 | spec
364 | rescue SignalException
365 | msg = 'Received signal, exiting'
366 | Utils.logger.error(msg)
367 | raise
368 | rescue StandardError => e
369 | msg = ["Unable to process #{gemfile}",
370 | "#{e.message} (#{e.class})",
371 | "\t#{e.backtrace.join "\n\t"}"].join("\n")
372 | Utils.logger.debug(msg)
373 | end
374 |
375 | results[index] = spec
376 | end
377 |
378 | # nils can result from insert by index
379 | results.compact
380 | end
381 |
382 | ##
383 | # Handle `index --update`, detecting changed files and file lists.
384 | #
385 | # @return nil
386 | #
387 | def update_index
388 | make_temp_directories
389 |
390 | present_gemfiles = Dir.glob('*.gem', base: File.join(@dest_directory, 'gems'))
391 | indexed_gemfiles = Dir.glob('*.gemspec.rz', base: @quick_marshal_dir_base).collect { |x| x.gsub(/spec.rz$/, '') }
392 |
393 | @updated_gems = []
394 |
395 | # detect files manually added to public/gems
396 | @updated_gems += (present_gemfiles - indexed_gemfiles).collect { |x| File.join(@dest_directory, 'gems', x) }
397 | # detect files manually deleted from public/gems
398 | @updated_gems += (indexed_gemfiles - present_gemfiles).collect { |x| File.join(@dest_directory, 'gems', x) }
399 |
400 | versions_mtime =
401 | begin
402 | File.stat(Dir.glob(File.join(@dest_directory, 'versions*.list')).last).mtime
403 | rescue StandardError
404 | Time.at(0)
405 | end
406 | newest_mtime = Time.at(0)
407 |
408 | # files that have been replaced
409 | @updated_gems +=
410 | gem_file_list.select do |gem|
411 | gem_mtime = File.stat(gem).mtime
412 | newest_mtime = gem_mtime if gem_mtime > newest_mtime
413 | gem_mtime > versions_mtime
414 | end
415 |
416 | @updated_gems.uniq!
417 |
418 | if @updated_gems.empty?
419 | Utils.logger.info('No new gems')
420 | terminate_interaction(0)
421 | end
422 |
423 | specs = map_gems_to_specs(@updated_gems)
424 |
425 | # specs only includes latest discovered files.
426 | # /info/[gemname] can not be rebuilt
427 | # incrementally, so retrive specs for all versions of these gems.
428 | gem_name_updates = specs.collect(&:name).uniq
429 | u2 =
430 | Dir.glob(File.join(File.join(@dest_directory, 'gems'), '*.gem')).select do |possibility|
431 | gem_name_updates.any? { |updated| File.basename(possibility) =~ /^#{updated}-\d/ }
432 | end
433 |
434 | Utils.logger.info('Reloading for /info/[gemname]')
435 | version_specs = map_gems_to_specs(u2)
436 |
437 | ::Gem::Specification.dirs = []
438 | ::Gem::Specification.all = *specs
439 | build_marshal_gemspecs specs
440 |
441 | build_compact_index_infos(version_specs, true)
442 | build_compact_index_versions(specs, true)
443 | build_compact_index_names
444 | end
445 |
446 | def download_source_versions
447 | Tempfile.create(File.basename(Gemirro.configuration.versions_file)) do |f|
448 | f.write(download_from_source('versions'))
449 | f.close
450 |
451 | FileUtils.rm(Gemirro.configuration.versions_file, verbose: verbose)
452 | FileUtils.cp(
453 | f.path,
454 | Gemirro.configuration.versions_file,
455 | verbose: verbose
456 | )
457 | end
458 | end
459 |
460 | def verbose
461 | @verbose ||= ::Gem.configuration.really_verbose
462 | end
463 | end
464 | end
465 |
--------------------------------------------------------------------------------
/lib/gemirro/mirror_directory.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The MirrorDirectory is used for dealing with files and directories that are
6 | # mirrored from an external source.
7 | #
8 | # @!attribute [r] path
9 | # @return [String]
10 | #
11 | class MirrorDirectory
12 | attr_reader :path
13 |
14 | ##
15 | # @param [String] path
16 | #
17 | def initialize(path)
18 | @path = path
19 | end
20 |
21 | ##
22 | # Creates directory or directories with the given path.
23 | #
24 | # @param [String] dir_path
25 | # @return [Gemirro::MirrorDirectory]
26 | #
27 | def add_directory(dir_path)
28 | full_path = File.join(@path, dir_path)
29 | FileUtils.mkdir_p(full_path) unless File.directory?(full_path)
30 |
31 | self.class.new(full_path)
32 | end
33 |
34 | ##
35 | # Creates a new file with the given name and content.
36 | #
37 | # @param [String] name
38 | # @param [String] content
39 | # @return [Gem::MirrorFile]
40 | #
41 | def add_file(name, content)
42 | full_path = File.join(@path, name)
43 | file = MirrorFile.new(full_path)
44 |
45 | file.write(content)
46 |
47 | file
48 | end
49 |
50 | ##
51 | # Checks if a given file exists in the current directory.
52 | #
53 | # @param [String] name
54 | # @return [TrueClass|FalseClass]
55 | #
56 | def file_exists?(name)
57 | File.file?(File.join(@path, name))
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/gemirro/mirror_file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # Similar to {Gemirro::MirrorDirectory} the MirrorFile class is used to
6 | # make it easier to read and write data in a directory that mirrors data from
7 | # an external source.
8 | #
9 | # @!attribute [r] path
10 | # @return [String]
11 | #
12 | class MirrorFile
13 | attr_reader :path
14 |
15 | ##
16 | # @param [String] path
17 | #
18 | def initialize(path)
19 | @path = path
20 | end
21 |
22 | ##
23 | # Writes the specified content to the current file. Existing files are
24 | # overwritten.
25 | #
26 | # @param [String] content
27 | #
28 | def write(content)
29 | FileUtils.mkdir_p(File.dirname(@path))
30 | handle = File.open(@path, 'w')
31 |
32 | handle.write(content)
33 | handle.close
34 | end
35 |
36 | ##
37 | # Reads the content of the current file.
38 | #
39 | # @return [String]
40 | #
41 | def read
42 | handle = File.open(@path, 'r')
43 | content = handle.read
44 |
45 | handle.close
46 |
47 | content
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/gemirro/server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'sinatra/base'
4 | require 'thin'
5 | require 'uri'
6 | require 'addressable/uri'
7 | require 'base64'
8 |
9 | module Gemirro
10 | ##
11 | # Launch Sinatra server to easily download gems.
12 | #
13 | class Server < Sinatra::Base
14 | access_logger = Logger.new(Utils.configuration.server.access_log).tap do |logger|
15 | ::Logger.class_eval { alias_method :write, :<< }
16 | logger.level = ::Logger::INFO
17 | end
18 |
19 | error_logger = File.new(Utils.configuration.server.error_log, 'a+')
20 | error_logger.sync = true
21 |
22 | before do
23 | env['rack.errors'] = error_logger
24 | Utils.configuration.logger = access_logger
25 | end
26 |
27 | ##
28 | # Configure server
29 | #
30 | configure do
31 | config = Utils.configuration
32 | config.server.host = 'localhost' if config.server.host.nil?
33 | config.server.port = '2000' if config.server.port.nil?
34 |
35 | set :static, true
36 |
37 | set :views, Gemirro::Configuration.views_directory
38 | set :port, config.server.port
39 | set :bind, config.server.host
40 | set :public_folder, config.destination.gsub(%r{/$}, '')
41 | set :environment, config.environment
42 | set :dump_errors, true
43 | set :raise_errors, true
44 |
45 | enable :logging
46 | use Rack::CommonLogger, access_logger
47 | end
48 |
49 | ##
50 | # Set template for not found action
51 | #
52 | # @return [nil]
53 | #
54 | not_found do
55 | content_type 'text/html'
56 | erb(:not_found)
57 | end
58 |
59 | ##
60 | # Display information about one gem, human readable
61 | #
62 | # @return [nil]
63 | #
64 | get('/gem/:gemname') do
65 | gems = Utils.gems_collection
66 | gem = gems.find_by_name(params[:gemname])
67 | return not_found if gem.nil?
68 |
69 | erb(:gem, {}, gem: gem)
70 | end
71 |
72 | ##
73 | # Display home page containing the list of gems already
74 | # downloaded on the server, human readable
75 | #
76 | # @return [nil]
77 | #
78 | get('/') do
79 | erb(:index, {}, gems: Utils.gems_collection)
80 | end
81 |
82 | ##
83 | # compact_index, Return list of available gem names
84 | #
85 | # @return [nil]
86 | #
87 | get '/names' do
88 | content_type 'text/plain'
89 |
90 | content_path = Dir.glob(File.join(Gemirro.configuration.destination, 'names.*.*.list')).last
91 | _, etag, repr_digest, _ = File.basename(content_path).split('.')
92 |
93 | headers 'etag' => %("#{etag}")
94 | headers 'repr-digest' => %(sha-256=#{Base64.strict_encode64([repr_digest].pack('H*'))})
95 | send_file content_path
96 | end
97 |
98 | ##
99 | # compact_index, Return list of gem, including versions
100 | #
101 | # @return [nil]
102 | #
103 | get '/versions' do
104 | content_type 'text/plain'
105 |
106 | content_path = Dir.glob(File.join(Utils.configuration.destination, 'versions.*.*.list')).last
107 | _, etag, repr_digest, _ = File.basename(content_path).split('.')
108 |
109 | headers 'etag' => %("#{etag}")
110 | headers 'repr-digest' => %(sha-256=#{Base64.strict_encode64([repr_digest].pack('H*'))})
111 | send_file content_path
112 | end
113 |
114 | # compact_index, Return gem dependencies for all versions of a gem
115 | #
116 | # @return [nil]
117 | #
118 | get('/info/:gemname') do
119 | gems = Utils.gems_collection
120 | gem = gems.find_by_name(params[:gemname])
121 | return not_found if gem.nil?
122 |
123 | content_type 'text/plain'
124 |
125 | content_path = Dir.glob(File.join(Utils.configuration.destination, 'info', "#{params[:gemname]}.*.*.list")).last
126 | _, etag, repr_digest, _ = File.basename(content_path).split('.')
127 |
128 | headers 'etag' => %("#{etag}")
129 | headers 'repr-digest' => %(sha-256=#{Base64.strict_encode64([repr_digest].pack('H*'))})
130 | send_file content_path
131 | end
132 |
133 | ##
134 | # Try to get all request and download files
135 | # if files aren't found.
136 | #
137 | # @return [nil]
138 | #
139 | get('*') do |path|
140 | resource = "#{settings.public_folder}#{path}"
141 |
142 | # Try to download gem
143 | Gemirro::Utils.fetch_gem(resource) unless File.exist?(resource)
144 | # If not found again, return a 404
145 | return not_found unless File.exist?(resource)
146 |
147 | send_file(resource)
148 | end
149 |
150 | ##
151 | # Compile fragments for /api/v1/dependencies
152 | #
153 | # @return [nil]
154 | #
155 | def dependencies_loader(names)
156 | names.collect do |name|
157 | f = File.join(settings.public_folder, 'api', 'v1', 'dependencies', "#{name}.*.*.list")
158 | Marshal.load(File.read(Dir.glob(f).last))
159 | rescue StandardError => e
160 | env['rack.errors'].write "Cound not open #{f}\n"
161 | env['rack.errors'].write "#{e.message}\n"
162 | e.backtrace.each do |err|
163 | env['rack.errors'].write "#{err}\n"
164 | end
165 | nil
166 | end
167 | .flatten
168 | .compact
169 | end
170 | end
171 | end
172 |
--------------------------------------------------------------------------------
/lib/gemirro/source.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The Source class is used for storing information about an external source
6 | # such as the name and the Gems to mirror.
7 | #
8 | # @!attribute [r] name
9 | # @return [String]
10 | # @!attribute [r] host
11 | # @return [String]
12 | # @!attribute [r] gems
13 | # @return [Array]
14 | #
15 | class Source
16 | attr_reader :name, :host, :gems
17 |
18 | ##
19 | # @param [String] name
20 | # @param [String] host
21 | # @param [Array] gems
22 | #
23 | def initialize(name, host, gems = [])
24 | @name = name.downcase.gsub(/\s+/, '_')
25 | @host = host.chomp('/')
26 | @gems = gems
27 | end
28 |
29 | ##
30 | # Fetches a list of all the available Gems and their versions.
31 | #
32 | # @return [String]
33 | #
34 | def fetch_versions
35 | Utils.logger.info(
36 | "Fetching versions on #{@name} (#{@host})"
37 | )
38 |
39 | Http.get("#{host}/versions").body
40 | end
41 |
42 | ##
43 | # Fetches the `.gem` file of a given Gem and version.
44 | #
45 | # @param [String] name
46 | # @param [String] version
47 | # @return [String]
48 | #
49 | def fetch_gem(filename)
50 | Utils.logger.info(
51 | "Fetching gem #{filename} on #{@host}"
52 | )
53 | Http.get(host + "/gems/#{filename}").body
54 | end
55 |
56 | ##
57 | # Fetches the `.gemspec.rz` file of a given Gem and version.
58 | #
59 | # @param [String] filename
60 | # @return [String]
61 | #
62 | def fetch_gemspec(filename)
63 | Utils.logger.info(
64 | "Fetching gemspec #{filename} on #{@host}"
65 | )
66 | marshal = Gemirro::Configuration.marshal_identifier
67 | Http.get(host + "/quick/#{marshal}/#{filename}").body
68 | end
69 |
70 | ##
71 | # Adds a new Gem to the source.
72 | #
73 | # @param [String] name
74 | # @param [String] requirement
75 | #
76 | def gem(name, requirement = nil)
77 | gems << Gem.new(name, requirement)
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/lib/gemirro/utils.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'gemirro/gem_version'
4 |
5 | module Gemirro
6 | ##
7 | # The Utils class is responsible for executing specific traitments
8 | # that are located at least on two other files
9 | #
10 | # @!attribute [r] client
11 | # @return [HTTPClient]
12 | # @!attribute [r] versions_fetcher
13 | # @return [VersionsFetcher]
14 | # @!attribute [r] gems_fetcher
15 | # @return [Gemirro::GemsFetcher]
16 | #
17 | class Utils
18 | attr_reader(
19 | :versions_fetcher,
20 | :gems_fetcher,
21 | :gems_collection,
22 | :stored_gems
23 | )
24 |
25 | URI_REGEXP = /^(.*)-(\d+(?:\.\d+){1,4}.*?)(?:-(x86-(?:(?:mswin|mingw)(?:32|64)).*?|java))?\.(gem(?:spec\.rz)?)$/
26 | GEMSPEC_TYPE = 'gemspec.rz'
27 | GEM_TYPE = 'gem'
28 |
29 | ##
30 | # Generate Gems collection from Marshal dump - always the .local file
31 | #
32 | # @return [Gemirro::GemVersionCollection]
33 | #
34 | def self.gems_collection
35 | @gems_collection ||= { files: {}, values: nil }
36 |
37 | file_paths =
38 | Dir.glob(File.join(
39 | Gemirro.configuration.destination,
40 | 'versions.*.*.list'
41 | ))
42 |
43 | has_file_changed =
44 | @gems_collection[:files] != file_paths.each_with_object({}) do |f, r|
45 | r[f] = File.mtime(f) if File.exist?(f)
46 | end
47 |
48 | # Return result if no file changed
49 | return @gems_collection[:values] if !has_file_changed && !@gems_collection[:values].nil?
50 |
51 | gems = []
52 |
53 | CompactIndex::VersionsFile.new(file_paths.last).contents.each_line.with_index do |line, index|
54 | next if index < 2
55 |
56 | gem_name = line.split[0]
57 | versions = line.split[1..-2].collect { |x| x.split(',') }.flatten # All except first and last
58 |
59 | versions.each do |ver|
60 | version, platform =
61 | if ver.include?('-')
62 | ver.split('-', 2)
63 | else
64 | [ver, 'ruby']
65 | end
66 |
67 | gems << Gemirro::GemVersion.new(gem_name, version, platform)
68 | end
69 | end
70 |
71 | @gems_collection[:values] = GemVersionCollection.new(gems)
72 | end
73 |
74 | ##
75 | # @see Gemirro::Configuration#logger
76 | # @return [Logger]
77 | #
78 | def self.logger
79 | configuration.logger
80 | end
81 |
82 | ##
83 | # @see Gemirro.configuration
84 | #
85 | def self.configuration
86 | Gemirro.configuration
87 | end
88 |
89 | ##
90 | # @see Gemirro::VersionsFetcher.fetch
91 | #
92 | def self.versions_fetcher
93 | @versions_fetcher ||= Gemirro::VersionsFetcher.new(configuration.source).fetch
94 | end
95 |
96 | ##
97 | # @return [Gemirro::GemsFetcher]
98 | #
99 | def self.gems_fetcher
100 | @gems_fetcher ||= Gemirro::GemsFetcher
101 | .new(configuration.source, versions_fetcher)
102 | end
103 |
104 | ##
105 | # Try to cache gem classes
106 | #
107 | # @param [String] gem_name Gem name
108 | # @return [Gem]
109 | #
110 | def self.stored_gem(gem_name, gem_version, platform = 'ruby')
111 | platform = 'ruby' if platform.nil?
112 | @stored_gems ||= {}
113 | @stored_gems[gem_name] = {} unless @stored_gems.key?(gem_name)
114 | @stored_gems[gem_name][gem_version] = {} unless @stored_gems[gem_name].key?(gem_version)
115 | unless @stored_gems[gem_name][gem_version].key?(platform)
116 | @stored_gems[gem_name][gem_version][platform] ||= Gem.new(gem_name, gem_version, platform)
117 | end
118 |
119 | @stored_gems[gem_name][gem_version][platform]
120 | end
121 |
122 | ##
123 | # Return gem specification from gemname and version
124 | #
125 | # @param [String] gemname
126 | # @param [String] version
127 | # @return [::Gem::Specification]
128 | #
129 | def self.spec_for(gemname, version, platform)
130 | gem = Utils.stored_gem(gemname, version.to_s, platform)
131 |
132 | spec_file =
133 | File.join(
134 | configuration.destination,
135 | 'quick',
136 | Gemirro::Configuration.marshal_identifier,
137 | gem.gemspec_filename
138 | )
139 |
140 | fetch_gem(spec_file) unless File.exist?(spec_file)
141 |
142 | # this is a separate action
143 | return unless File.exist?(spec_file)
144 |
145 | File.open(spec_file, 'r') do |uz_file|
146 | uz_file.binmode
147 | inflater = Zlib::Inflate.new
148 | begin
149 | inflate_data = inflater.inflate(uz_file.read)
150 | ensure
151 | inflater.finish
152 | inflater.close
153 | end
154 |
155 | Marshal.load(inflate_data)
156 | end
157 | end
158 |
159 | ##
160 | # Try to fetch gem and download its if it's possible, and
161 | # build and install indicies.
162 | #
163 | # @param [String] resource
164 | # @return [Indexer]
165 | #
166 | def self.fetch_gem(resource)
167 | return unless Utils.configuration.fetch_gem
168 |
169 | name = File.basename(resource)
170 | result = name.match(URI_REGEXP)
171 | return unless result
172 |
173 | gem_name, gem_version, gem_platform, gem_type = result.captures
174 | return unless gem_name && gem_version
175 |
176 | begin
177 | gem = Utils.stored_gem(gem_name, gem_version, gem_platform)
178 | gem.gemspec = true if gem_type == GEMSPEC_TYPE
179 |
180 | return if Utils.gems_fetcher.gem_exists?(gem.filename(gem_version)) && gem_type == GEM_TYPE
181 | return if Utils.gems_fetcher.gemspec_exists?(gem.gemspec_filename(gem_version)) && gem_type == GEMSPEC_TYPE
182 |
183 | Utils.logger.info("Try to download #{gem_name} with version #{gem_version}")
184 | Utils.gems_fetcher.source.gems.clear
185 | Utils.gems_fetcher.source.gems.push(gem)
186 | Utils.gems_fetcher.fetch
187 |
188 | update_indexes if Utils.configuration.update_on_fetch
189 | rescue StandardError => e
190 | Utils.logger.error(e)
191 | end
192 | end
193 |
194 | ##
195 | # Update indexes files
196 | #
197 | # @return [Indexer]
198 | #
199 | def self.update_indexes
200 | indexer = Gemirro::Indexer.new
201 | indexer.only_origin = true
202 | indexer.ui = ::Gem::SilentUI.new
203 |
204 | Utils.logger.info('Generating indexes')
205 | indexer.update_index
206 | rescue SystemExit => e
207 | Utils.logger.info(e.message)
208 | end
209 | end
210 | end
211 |
--------------------------------------------------------------------------------
/lib/gemirro/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Gemirro Version
4 | module Gemirro
5 | VERSION = '2.0.0'
6 | end
7 |
--------------------------------------------------------------------------------
/lib/gemirro/versions_fetcher.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The VersionsFetcher class is used for retrieving the file that contains all
6 | # registered Gems and their versions.
7 | #
8 | # @!attribute [r] source
9 | # @return [Source]
10 | #
11 | class VersionsFetcher
12 | attr_reader :source
13 |
14 | ##
15 | # @param [Source] source
16 | #
17 | def initialize(source)
18 | @source = source
19 | end
20 |
21 | ##
22 | # @return [Gemirro::VersionsFile]
23 | #
24 | def fetch
25 | return unless Gemirro.configuration.versions_file
26 |
27 | VersionsFile.new(read_file(Gemirro.configuration.versions_file))
28 | end
29 |
30 | ##
31 | # Read file if exists otherwise download its from source
32 | #
33 | # @param [String] file name
34 | #
35 | def read_file(file)
36 | unless File.exist?(file)
37 | throw 'No source defined' unless @source
38 |
39 | File.write(file, @source.fetch_versions)
40 | end
41 |
42 | File.read(file)
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/gemirro/versions_file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Gemirro
4 | ##
5 | # The VersionsFile class acts as a small Ruby wrapper around the RubyGems
6 | # file that contains all Gems and their associated versions.
7 | #
8 | # @!attribute [r] versions
9 | # @return [Array]
10 | # @!attribute [r] versions_hash
11 | # @return [Hash]
12 | #
13 | class VersionsFile
14 | attr_reader :versions_string, :versions_hash
15 |
16 | ##
17 | # Reads the versions file from the specified String.
18 | #
19 | # @param [String] versions_content
20 | # @return [Gemirro::VersionsFile]
21 | #
22 |
23 | ##
24 | # @param [String] versions
25 | #
26 | def initialize(versions_string)
27 | unless versions_string.is_a? String
28 | throw "#{versions_string.class} is wrong format, expect String; #{versions_string.inspect}"
29 | end
30 |
31 | @versions_string = versions_string
32 | @versions_hash = create_versions_hash
33 | end
34 |
35 | ##
36 | # Creates a Hash based on the Array containing all versions. This Hash is
37 | # used to more easily (and faster) iterate over all the gems/versions.
38 | #
39 | # @return [Hash]
40 | #
41 | def create_versions_hash
42 | hash = Hash.new { |h, k| h[k] = [] }
43 |
44 | versions_string.each_line.with_index do |line, index|
45 | next if index < 2
46 |
47 | parts = line.split
48 | gem_name = parts[0]
49 | parts[-1]
50 | versions = parts[1..-2].collect { |x| x.split(',') }.flatten # All except first and last
51 |
52 | versions.each do |ver|
53 | version, platform =
54 | if ver.include?('-')
55 | ver.split('-', 2)
56 | else
57 | [ver, 'ruby']
58 | end
59 | hash[gem_name] << [gem_name, ::Gem::Version.new(version), platform]
60 | end
61 | end
62 | hash
63 | end
64 |
65 | ##
66 | # Returns an Array containing all the available versions for a Gem.
67 | #
68 | # @param [String] gem
69 | # @return [Array]
70 | #
71 | def versions_for(gem)
72 | versions_hash[gem].map { |version| [version[1], version[2]] }
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/spec/fixtures/gems/gemirro-0.0.1.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PierreRambaud/gemirro/b5ffe34c99daadb1f4bdddb5d354cf51043a9088/spec/fixtures/gems/gemirro-0.0.1.gem
--------------------------------------------------------------------------------
/spec/fixtures/quick/gemirro-0.0.1.gemspec.rz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PierreRambaud/gemirro/b5ffe34c99daadb1f4bdddb5d354cf51043a9088/spec/fixtures/quick/gemirro-0.0.1.gemspec.rz
--------------------------------------------------------------------------------
/spec/gemirro/cli_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/cli'
3 | require 'gemirro/mirror_file'
4 | require 'slop'
5 |
6 | # Gemirro tests
7 | module Gemirro
8 | # CLI tests
9 | module CLI
10 | describe 'CLI' do
11 | include FakeFS::SpecHelpers
12 |
13 | it 'should return options' do
14 | options = CLI.options
15 | expect(options).to be_a(::Slop)
16 | expect(options.config[:strict]).to be_truthy
17 | expect(options.config[:banner])
18 | .to eq('Usage: gemirro [COMMAND] [OPTIONS]')
19 | expect(options.to_s)
20 | .to match(/-v, --version(\s+)Shows the current version/)
21 | expect(options.to_s)
22 | .to match(/-h, --help(\s+)Display this help message./)
23 |
24 | version = options.fetch_option(:v)
25 | expect(version.short).to eq('v')
26 | expect(version.long).to eq('version')
27 | expect { version.call }.to output(/gemirro v.* on ruby/).to_stdout
28 | end
29 |
30 | it 'should retrieve version information' do
31 | expect(CLI.version_information).to eq(
32 | "gemirro v#{VERSION} on #{RUBY_DESCRIPTION}"
33 | )
34 | end
35 |
36 | it 'should raise SystemExit if file does not exists' do
37 | allow(CLI).to receive(:abort)
38 | .with('The configuration file /config.rb does not exist')
39 | .and_raise SystemExit
40 | expect { CLI.load_configuration('config.rb') }.to raise_error SystemExit
41 | end
42 |
43 | it 'should raise LoadError if content isn\'t ruby' do
44 | file = MirrorFile.new('./config.rb')
45 | file.write('test')
46 | expect { CLI.load_configuration('config.rb') }.to raise_error LoadError
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/gemirro/configuration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/mirror_directory'
3 | require 'gemirro/configuration'
4 | require 'gemirro/source'
5 |
6 | # Configuration tests
7 | module Gemirro
8 | describe 'Configuration' do
9 | include FakeFS::SpecHelpers
10 |
11 | it 'should return configuration' do
12 | expect(Gemirro.configuration).to be_a Configuration
13 | end
14 |
15 | it 'should return logger' do
16 | expect(Gemirro.configuration.logger).to be_a Logger
17 | end
18 |
19 | it 'should return template directory' do
20 | FakeFS::FileSystem.clone(
21 | File.expand_path(
22 | './',
23 | Pathname.new(__FILE__).realpath
24 | )
25 | )
26 | expect(Configuration.template_directory).to eq(
27 | File.expand_path(
28 | '../../../template',
29 | Pathname.new(__FILE__).realpath
30 | )
31 | )
32 | end
33 |
34 | it 'should return default config file' do
35 | expect(Configuration.default_configuration_file).to eq('/config.rb')
36 | end
37 |
38 | it 'should return marshal identifier' do
39 | expect(Configuration.marshal_identifier).to match(/Marshal\.(\d+)\.(\d+)/)
40 | end
41 |
42 | it 'should error on version since no source is defined' do
43 | expect(Gemirro.configuration.versions_file).to match(nil)
44 | end
45 |
46 | it 'should return marshal file' do
47 | expect(Configuration.marshal_version).to eq(
48 | "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}"
49 | )
50 | end
51 | end
52 |
53 | describe 'Configuration::instance' do
54 | before(:each) do
55 | @config = Configuration.new
56 | end
57 |
58 | it 'return mirror directory' do
59 | allow(@config).to receive(:gems_directory).once.and_return('/tmp')
60 | expect(@config.mirror_gems_directory).to be_a(MirrorDirectory)
61 | expect(@config.mirror_gems_directory.path).to eq('/tmp')
62 | end
63 |
64 | it 'should return gems directory' do
65 | allow(@config).to receive(:destination).once.and_return('/tmp')
66 | expect(@config.gems_directory).to eq('/tmp/gems')
67 | end
68 |
69 | it 'should return ignored gems' do
70 | expect(@config.ignored_gems).to eq({})
71 | expect(@config.ignore_gem?('rake', '1.0.0', 'ruby')).to be_falsy
72 | expect(@config.ignore_gem('rake', '1.0.0', 'ruby')).to eq(['1.0.0'])
73 | expect(@config.ignored_gems).to eq('ruby' => {'rake' => ['1.0.0']})
74 | expect(@config.ignore_gem?('rake', '1.0.0', 'ruby')).to be_truthy
75 | expect(@config.ignore_gem?('rake', '1.0.0', 'java')).to be_falsy
76 | end
77 |
78 | it 'should add and return source' do
79 | expect(@config.source).to eq(nil)
80 | result = @config.define_source('RubyGems', 'https://rubygems.org') do
81 | end
82 | expect(result).to be_a(Source)
83 | expect(result.gems).to eq([])
84 | expect(result.host).to eq('https://rubygems.org')
85 | expect(result.name).to eq('rubygems')
86 | end
87 |
88 |
89 | it 'should return versions file with source' do
90 | result = @config.define_source('RubyGems', 'https://rubygems.org') do
91 | end
92 | expect(@config.versions_file).to match(/rubygems_org_versions$/)
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/spec/gemirro/gem_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/gem'
3 |
4 | # Gem tests
5 | module Gemirro
6 | describe 'Gem' do
7 | before(:each) do
8 | @gem = Gem.new('gemirro', '0.0.1')
9 | end
10 |
11 | it 'should be initialized with string' do
12 | gem = Gem.new('gemirro', '0.0.1')
13 | expect(gem.name).to eq('gemirro')
14 | expect(gem.requirement).to eq(::Gem::Requirement.new(['= 0.0.1']))
15 | end
16 |
17 | it 'should be initialized with ::Gem::Requirement' do
18 | requirement = ::Gem::Requirement.new('0.0.1')
19 | gem = Gem.new('gemirro', requirement)
20 | expect(gem.name).to eq('gemirro')
21 | expect(gem.requirement).to be(requirement)
22 | end
23 |
24 | it 'should return version' do
25 | expect(@gem.version).to eq(::Gem::Version.new('0.0.1'))
26 | end
27 |
28 | it 'should check version' do
29 | expect(@gem.version?).to be_truthy
30 | end
31 |
32 | it 'should return gem filename' do
33 | expect(@gem.filename).to eq('gemirro-0.0.1.gem')
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/gemirro/gem_version_collection_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/gem_version_collection'
3 | require 'gemirro/gem_version'
4 |
5 | # Gem tests
6 | module Gemirro
7 | describe 'GemVersionCollection' do
8 | it 'should be initialized' do
9 | collection = GemVersionCollection.new([['subzero',
10 | '0.0.1',
11 | 'ruby'],
12 | GemVersion.new('alumina',
13 | '0.0.1',
14 | 'ruby')])
15 | expect(collection.gems.first).to be_a(GemVersion)
16 | expect(collection.gems.last).to be_a(GemVersion)
17 | expect(collection.oldest).to be(collection.gems.first)
18 | expect(collection.newest).to be(collection.gems.last)
19 | expect(collection.size).to eq(2)
20 | end
21 |
22 | it 'should group and sort gems' do
23 | collection = GemVersionCollection.new([['subzero',
24 | '0.0.1',
25 | 'ruby'],
26 | GemVersion.new('alumina',
27 | '0.0.1',
28 | 'ruby')])
29 | expect(collection.by_name.first[0]).to eq('alumina')
30 | values = %w[alumina subzero]
31 | collection.by_name do |name, _version|
32 | expect(name).to eq(values.shift)
33 | end
34 | end
35 |
36 | it 'should find gem by name' do
37 | collection = GemVersionCollection.new([['subzero',
38 | '0.0.1',
39 | 'ruby'],
40 | GemVersion.new('alumina',
41 | '0.0.1',
42 | 'ruby'),
43 | GemVersion.new('alumina',
44 | '0.0.2',
45 | 'ruby')])
46 | expect(collection.find_by_name('something')).to be_nil
47 | expect(collection.find_by_name('alumina').newest.name)
48 | .to eq('alumina')
49 | expect(collection.find_by_name('alumina').newest.version.to_s)
50 | .to eq('0.0.2')
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/gemirro/gem_version_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/gem_version'
3 |
4 | # Gem tests
5 | module Gemirro
6 | describe 'GemVersion' do
7 | it 'should be initialized' do
8 | gem = GemVersion.new('gemirro',
9 | '0.0.1',
10 | 'ruby')
11 | expect(gem.name).to eq('gemirro')
12 | expect(gem.number).to eq('0.0.1')
13 | expect(gem.platform).to eq('ruby')
14 | expect(gem.ruby?).to be_truthy
15 | expect(gem.version).to be_a(::Gem::Version)
16 | expect(gem.gemfile_name).to eq('gemirro-0.0.1')
17 | end
18 |
19 | it 'should be initialized with other platform' do
20 | gem = GemVersion.new('gemirro',
21 | '0.0.1',
22 | 'jruby')
23 | expect(gem.name).to eq('gemirro')
24 | expect(gem.number).to eq('0.0.1')
25 | expect(gem.platform).to eq('jruby')
26 | expect(gem.ruby?).to be_falsy
27 | expect(gem.version).to be_a(::Gem::Version)
28 | expect(gem.gemfile_name).to eq('gemirro-0.0.1-jruby')
29 | end
30 |
31 | it 'should compare with an other gem' do
32 | first_gem = GemVersion.new('gemirro',
33 | '0.0.1',
34 | 'ruby')
35 | second_gem = GemVersion.new('gemirro',
36 | '0.0.2',
37 | 'ruby')
38 | third_gem = GemVersion.new('gemirro',
39 | '0.0.1',
40 | 'ruby')
41 | expect(first_gem < second_gem).to eq(true)
42 | expect(second_gem < first_gem).to eq(false)
43 | expect(first_gem == third_gem).to eq(true)
44 | expect(first_gem != second_gem).to eq(true)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/gemirro/gems_fetcher_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/source'
3 | require 'gemirro/gem'
4 | require 'gemirro/versions_file'
5 | require 'gemirro/mirror_file'
6 | require 'gemirro/gems_fetcher'
7 |
8 | # GemsFetcher tests
9 | module Gemirro
10 | describe 'GemsFetcher' do
11 | include FakeFS::SpecHelpers
12 |
13 | before(:each) do
14 | @source = Source.new('RubyGems', 'https://rubygems.org')
15 | @versions_file = VersionsFile.new(%(created_at: 2025-04-24T03:46:59Z\n---\nrack 3.0.0,3.0.1 d545a45462d63b1b4865bbb89a109366))
16 | @fetcher = GemsFetcher.new(@source, @versions_file)
17 | Gemirro.configuration.ignored_gems.clear
18 | end
19 |
20 | it 'should be initialized' do
21 | expect(@fetcher.source).to be(@source)
22 | expect(@fetcher.versions_file).to be(@versions_file)
23 | end
24 |
25 | it 'should test if gem exists' do
26 | Utils.configuration.destination = './'
27 | expect(@fetcher.gem_exists?('test')).to be_falsy
28 | MirrorDirectory.new('./').add_directory('gems')
29 | MirrorDirectory.new('./').add_directory('quick/Marshal.4.8')
30 | MirrorFile.new('gems/test').write('content')
31 | expect(@fetcher.gem_exists?('test')).to be_truthy
32 | end
33 |
34 | it 'should ignore gem' do
35 | allow(Utils.logger).to receive(:info)
36 | .once.with('Fetching gemirro-0.0.1.gem')
37 | expect(@fetcher.ignore_gem?('gemirro', '0.0.1', 'ruby')).to be_falsy
38 | Utils.configuration.ignore_gem('gemirro', '0.0.1', 'ruby')
39 | expect(@fetcher.ignore_gem?('gemirro', '0.0.1', 'ruby')).to be_truthy
40 | expect(@fetcher.ignore_gem?('gemirro', '0.0.1', 'java')).to be_falsy
41 | end
42 |
43 | it 'should log error when fetch gem failed' do
44 | allow(Utils.logger).to receive(:info)
45 | .once.with('Fetching gemirro-0.0.1.gem')
46 | gem = Gem.new('gemirro')
47 | version = ::Gem::Version.new('0.0.1')
48 | Utils.configuration.ignore_gem('gemirro', '0.0.1', 'ruby')
49 | allow(@source).to receive(:fetch_gem)
50 | .once.with('gemirro', version).and_raise(ArgumentError)
51 | allow(Utils.logger).to receive(:error)
52 | .once.with(/Failed to retrieve/)
53 | allow(Utils.logger).to receive(:debug)
54 | .once.with(/Adding (.*) to the list of ignored Gems/)
55 |
56 | expect(@fetcher.fetch_gem(gem, version)).to be_nil
57 | expect(@fetcher.ignore_gem?('gemirro', '0.0.1', 'ruby')).to be_truthy
58 | end
59 |
60 | it 'should fetch gem' do
61 | allow(Utils.logger).to receive(:info)
62 | .once.with('Fetching gemirro-0.0.1.gem')
63 | MirrorDirectory.new('./').add_directory('gems')
64 | gem = Gem.new('gemirro')
65 | version = ::Gem::Version.new('0.0.1')
66 | allow(@source).to receive(:fetch_gem)
67 | .with('gemirro-0.0.1.gem').and_return('gemirro')
68 |
69 | expect(@fetcher.fetch_gem(gem, version)).to eq('gemirro')
70 | end
71 |
72 | it 'should fetch latest gem' do
73 | allow(Utils.logger).to receive(:info)
74 | .once.with('Fetching gemirro-0.0.1.gem')
75 | MirrorDirectory.new('./').add_directory('gems')
76 | gem = Gem.new('gemirro', :latest)
77 | version = ::Gem::Version.new('0.0.1')
78 | allow(@source).to receive(:fetch_gem)
79 | .with('gemirro-0.0.1.gem').and_return('gemirro')
80 |
81 | expect(@fetcher.fetch_gem(gem, version)).to eq('gemirro')
82 | end
83 |
84 | it 'should fetch gemspec' do
85 | allow(Utils.logger).to receive(:info)
86 | .once.with('Fetching gemirro-0.0.1.gemspec.rz')
87 | MirrorDirectory.new('./').add_directory('quick/Marshal.4.8')
88 | gem = Gem.new('gemirro')
89 | gem.gemspec = true
90 | version = ::Gem::Version.new('0.0.1')
91 | allow(@source).to receive(:fetch_gemspec)
92 | .once.with('gemirro-0.0.1.gemspec.rz').and_return('gemirro')
93 |
94 | expect(@fetcher.fetch_gemspec(gem, version)).to eq('gemirro')
95 | end
96 |
97 | it 'should fetch latest gemspec' do
98 | allow(Utils.logger).to receive(:info)
99 | .once.with('Fetching gemirro-0.0.1.gemspec.rz')
100 | MirrorDirectory.new('./').add_directory('quick/Marshal.4.8')
101 | gem = Gem.new('gemirro', :latest)
102 | gem.gemspec = true
103 | version = ::Gem::Version.new('0.0.1')
104 | allow(@source).to receive(:fetch_gemspec)
105 | .once.with('gemirro-0.0.1.gemspec.rz').and_return('gemirro')
106 |
107 | expect(@fetcher.fetch_gemspec(gem, version)).to eq('gemirro')
108 | end
109 |
110 | it 'should not fetch gemspec if file exists' do
111 | allow(Utils.logger).to receive(:info)
112 | .once.with('Fetching gemirro-0.0.1.gemspec.rz')
113 | allow(@fetcher).to receive(:gemspec_exists?)
114 | .once.with('gemirro-0.0.1.gemspec.rz')
115 | .and_return(true)
116 | allow(Utils.logger).to receive(:debug)
117 | .once.with('Skipping gemirro-0.0.1.gemspec.rz')
118 |
119 | gem = Gem.new('gemirro')
120 | gem.gemspec = true
121 | version = ::Gem::Version.new('0.0.1')
122 |
123 | expect(@fetcher.fetch_gemspec(gem, version)).to be_nil
124 | end
125 |
126 | it 'should retrieve versions for specific gem' do
127 | gem = Gem.new('gemirro', '0.0.2')
128 | allow(@versions_file).to receive(:versions_for)
129 | .once.with('gemirro')
130 | .and_return([[::Gem::Version.new('0.0.1'), 'ruby'],
131 | [::Gem::Version.new('0.0.2'), 'ruby']])
132 | expect(@fetcher.versions_for(gem)).to eq([[::Gem::Version.new('0.0.2'), 'ruby']])
133 | end
134 |
135 | it 'should fetch all gems and log debug if gem is not satisfied' do
136 | MirrorDirectory.new('./').add_directory('gems')
137 | gem = Gem.new('gemirro', '0.0.1')
138 | allow(gem.requirement).to receive(:satisfied_by?)
139 | .once.with(nil).and_return(false)
140 | @fetcher.source.gems << gem
141 | allow(Utils.logger).to receive(:debug)
142 | .once.with('Skipping gemirro-0.0.1.gem')
143 | expect(@fetcher.fetch).to eq([gem])
144 | end
145 |
146 | it 'should fetch all gems' do
147 | gem = Gem.new('gemirro', '0.0.2')
148 | @fetcher.source.gems << gem
149 | gemspec = Gem.new('gemirro', '0.0.1')
150 | gemspec.gemspec = true
151 | @fetcher.source.gems << gemspec
152 |
153 | allow(@fetcher).to receive(:fetch_gemspec)
154 | .once.with(gemspec, nil).and_return('gemfile')
155 | allow(@fetcher).to receive(:fetch_gem)
156 | .once.with(gem, nil).and_return('gemfile')
157 |
158 | allow(Utils.configuration.mirror_gems_directory).to receive(:add_file)
159 | .once.with('gemirro-0.0.2.gem', 'gemfile')
160 | allow(Utils.configuration.mirror_gemspecs_directory)
161 | .to receive(:add_file)
162 | .once.with('gemirro-0.0.1.gemspec.rz', 'gemfile')
163 | expect(@fetcher.fetch).to eq([gem, gemspec])
164 | end
165 | end
166 | end
167 |
--------------------------------------------------------------------------------
/spec/gemirro/http_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'httpclient'
3 | require 'gemirro/http'
4 |
5 | module Gemirro
6 | describe Http do
7 | let(:config) { double('Configuration') }
8 |
9 | before do
10 | Http.instance_variable_set(:@client, nil)
11 | allow(Utils).to receive(:configuration).and_return(config)
12 | end
13 |
14 | describe '.client' do
15 | it 'initializes a new HTTPClient' do
16 | expect(Http.client).to be_a(HTTPClient)
17 | end
18 |
19 | context 'with proxy configuration' do
20 | it 'sets proxy configuration' do
21 | allow(config).to receive(:proxy).and_return('http://proxy.example.com:8080')
22 |
23 | expect(Http.client.proxy.to_s).to eq('http://proxy.example.com:8080')
24 | end
25 | end
26 |
27 | context 'with SSL configuration' do
28 | context 'with invalid root CA path' do
29 | before do
30 | allow(config).to receive(:rootca).and_return('/nonexistent/ca.crt')
31 | allow(File).to receive(:file?).with('/nonexistent/ca.crt').and_return(false)
32 | end
33 |
34 | it 'aborts with error message' do
35 | expect { Http.client }.to raise_error(SystemExit)
36 | end
37 | end
38 |
39 | context 'with verify_mode disabled' do
40 | before do
41 | allow(config).to receive(:verify_mode).and_return(false)
42 | end
43 |
44 | it 'sets SSL verify mode to VERIFY_NONE' do
45 | expect(Http.client.ssl_config.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE)
46 | end
47 | end
48 | end
49 |
50 | context 'with basic auth forced' do
51 | before do
52 | allow(config).to receive(:basic_auth).and_return(true)
53 | end
54 |
55 | it 'forces basic authentication' do
56 | expect(Http.client.www_auth.basic_auth.force_auth).to be true
57 | end
58 | end
59 | end
60 |
61 | describe '.get' do
62 | let(:client) { instance_double(HTTPClient) }
63 |
64 | before do
65 | allow(Http).to receive(:client).and_return(client)
66 | end
67 |
68 | context 'with successful response' do
69 | let(:response) { double('Response', status: 200, body: 'content') }
70 |
71 | it 'returns response for successful request' do
72 | allow(client).to receive(:get)
73 | .with('http://example.com', follow_redirect: true)
74 | .and_return(response)
75 |
76 | expect(Http.get('http://example.com')).to eq(response)
77 | end
78 | end
79 |
80 | context 'with error response' do
81 | let(:response) { double('Response', status: 404, reason: 'Not Found') }
82 |
83 | it 'raises BadResponseError for failed request' do
84 | allow(client).to receive(:get)
85 | .with('http://example.com', follow_redirect: true)
86 | .and_return(response)
87 |
88 | expect { Http.get('http://example.com') }
89 | .to raise_error(HTTPClient::BadResponseError, 'Not Found')
90 | end
91 | end
92 | end
93 | end
94 | end
--------------------------------------------------------------------------------
/spec/gemirro/indexer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'builder'
3 | require 'rubygems/indexer'
4 | require 'tempfile'
5 | require 'gemirro/source'
6 | require 'gemirro/indexer'
7 | require 'gemirro/mirror_file'
8 | require 'gemirro/mirror_directory'
9 |
10 | # Indexer tests
11 | module Gemirro
12 | describe 'Indexer' do
13 | include FakeFS::SpecHelpers
14 |
15 | before(:each) do
16 | allow_any_instance_of(Logger).to receive(:info)
17 | allow_any_instance_of(Logger).to receive(:debug)
18 | allow_any_instance_of(Logger).to receive(:warn)
19 | end
20 |
21 | it 'should download from source' do
22 | source = Source.new('Rubygems', 'https://rubygems.org')
23 | allow(Gemirro.configuration).to receive(:source).and_return(source)
24 |
25 | dir = MirrorDirectory.new('/tmp')
26 | dir.add_directory('test')
27 | indexer = Indexer.new
28 |
29 | Struct.new('HttpGet', :code, :body)
30 | http_get = Struct::HttpGet.new(200, 'bad')
31 | allow(Http).to receive(:get).once.and_return(http_get)
32 |
33 | expect(indexer.download_from_source('something'))
34 | .to eq('bad')
35 | end
36 |
37 | it 'should install indices' do
38 | allow(Gemirro.configuration).to receive(:destination).and_return('/tmp')
39 |
40 | dir = MirrorDirectory.new('/tmp')
41 | dir.add_directory('test')
42 | dir.add_directory('gem_generate_index/quick/Marshal.4.8')
43 | allow(::Gem.configuration).to receive(:really_verbose)
44 | .once.and_return(true)
45 |
46 | indexer = Indexer.new
47 | indexer.quick_marshal_dir = '/tmp/gem_generate_index/quick/Marshal.4.8'
48 | indexer.dest_directory = '/tmp/test'
49 | indexer.directory = '/tmp/gem_generate_index'
50 | indexer.instance_variable_set('@specs_index',
51 | '/tmp/gem_generate_index/specs.4.8')
52 | indexer.files = [
53 | '/tmp/gem_generate_index/quick/Marshal.4.8',
54 | '/tmp/gem_generate_index/specs.4.8.gz',
55 | '/tmp/gem_generate_index/something.4.8.gz'
56 | ]
57 | allow(FileUtils).to receive(:mkdir_p).and_return(true)
58 | allow(FileUtils).to receive(:rm_f).and_return(true)
59 | allow(FileUtils).to receive(:mkdir_p)
60 | .once.with('/tmp/test/quick', verbose: true)
61 | allow(FileUtils).to receive(:rm_rf)
62 | .once.with('/tmp/gem_generate_index/something.4.8.gz')
63 | allow(FileUtils).to receive(:rm_rf)
64 | .once.with('/tmp/gem_generate_index/specs.4.8.gz')
65 | allow(FileUtils).to receive(:rm_rf)
66 | .once.with('/tmp/test/quick/Marshal.4.8', verbose: true)
67 | allow(FileUtils).to receive(:mv)
68 | .once
69 | .with('/tmp/gem_generate_index/quick/Marshal.4.8',
70 | '/tmp/test/quick/Marshal.4.8',
71 | verbose: true, force: true)
72 |
73 | allow(FileUtils).to receive(:mv)
74 |
75 | source = Source.new('Rubygems', 'https://rubygems.org')
76 | allow(Gemirro.configuration).to receive(:source).and_return(source)
77 |
78 | wio = StringIO.new('w')
79 | w_gz = Zlib::GzipWriter.new(wio)
80 | w_gz.write(['content'])
81 | w_gz.close
82 | allow(indexer).to receive(:download_from_source).and_return(wio.string)
83 |
84 | allow(Marshal).to receive(:load).and_return(['content'])
85 | allow(Marshal).to receive(:dump).and_return(['content'])
86 |
87 | Struct.new('GzipReader', :read)
88 | gzip_reader = Struct::GzipReader.new(wio.string)
89 | allow(Zlib::GzipReader).to receive(:open)
90 | .once
91 | .with('/tmp/gem_generate_index/specs.4.8.gz')
92 | .and_return(gzip_reader)
93 |
94 | files = indexer.install_indices
95 | expect(files).to eq(['/tmp/gem_generate_index/specs.4.8.gz',
96 | '/tmp/gem_generate_index/something.4.8.gz'])
97 | end
98 |
99 | it 'should build indices' do
100 | indexer = Indexer.new
101 | dir = MirrorDirectory.new('/')
102 | dir.add_directory('gems')
103 | dir.add_directory('quick')
104 | dir.add_directory('tmp')
105 | dir.add_directory("#{indexer.directory.gsub(%r{^/}, '')}/gems")
106 | dir.add_directory("#{indexer.directory.gsub(%r{^/}, '')}/quick")
107 |
108 | fixtures_dir = File.dirname(__FILE__) + '/../fixtures'
109 | FakeFS::FileSystem
110 | .clone("#{fixtures_dir}/gems/gemirro-0.0.1.gem",
111 | "#{indexer.directory}/gems/gemirro-0.0.1.gem")
112 | FakeFS::FileSystem
113 | .clone("#{fixtures_dir}/gems/gemirro-0.0.1.gem",
114 | '/gems/gemirro-0.0.1.gem')
115 | FakeFS::FileSystem
116 | .clone("#{fixtures_dir}/gems/gemirro-0.0.1.gem",
117 | '/gems/gemirral-0.0.1.gem') # Skipping misnamed
118 | FakeFS::FileSystem
119 | .clone("#{fixtures_dir}/quick/gemirro-0.0.1.gemspec.rz",
120 | "#{indexer.directory}/quick/gemirro-0.0.1.gemspec.rz")
121 |
122 | MirrorFile.new('gems/gemirro-0.0.2.gem').write('') # Empty file
123 | MirrorFile.new('gems/gemirro-0.0.3.gem').write('Error') # Empty file
124 | MirrorFile.new('/specs.4.8').write('')
125 |
126 | allow(indexer).to receive(:gem_file_list)
127 | .and_return(['gems/gemirro-0.0.1.gem',
128 | 'gems/gemirro-0.0.2.gem',
129 | 'gems/gemirro-0.0.3.gem',
130 | 'gems/gemirral-0.0.1.gem'])
131 |
132 | allow(indexer).to receive(:build_marshal_gemspecs)
133 | .once
134 | .and_return(["#{indexer.directory}/quick/gemirro-0.0.1.gemspec.rz"])
135 |
136 | allow(indexer).to receive(:compress_indicies).once.and_return(true)
137 | allow(indexer).to receive(:compress_indices).once.and_return(true)
138 |
139 | indexer.build_indices
140 | end
141 |
142 | it 'should update index and exit ruby gems' do
143 | indexer = Indexer.new
144 | MirrorDirectory.new('/')
145 | MirrorFile.new('/specs.4.8').write('')
146 | expect { indexer.update_index }.to raise_error(::Gem::SystemExitException)
147 | end
148 |
149 | it 'should update index' do
150 | dir = MirrorDirectory.new('/tmp')
151 | dir.add_directory('gem_generate_index/quick/Marshal.4.8')
152 | dir.add_directory('test/gems')
153 | dir.add_directory('test/quick')
154 |
155 | indexer = Indexer.new
156 | indexer.quick_marshal_dir = '/tmp/gem_generate_index/quick/Marshal.4.8'
157 | indexer.dest_directory = '/tmp/test'
158 | indexer.directory = '/tmp/gem_generate_index'
159 | indexer.instance_variable_set('@specs_index',
160 | '/tmp/gem_generate_index/specs.4.8')
161 |
162 | MirrorFile.new("#{indexer.directory}/specs.4.8.gz").write('')
163 | MirrorFile.new("#{indexer.directory}/specs.4.8").write('')
164 | MirrorFile.new("#{indexer.dest_directory}/specs.4.8").write('')
165 | File.utime(Time.at(0), Time.at(0), "#{indexer.dest_directory}/specs.4.8")
166 |
167 | fixtures_dir = File.dirname(__FILE__) + '/../fixtures'
168 | FakeFS::FileSystem
169 | .clone("#{fixtures_dir}/gems/gemirro-0.0.1.gem",
170 | "#{indexer.dest_directory}/gems/gemirro-0.0.1.gem")
171 | FakeFS::FileSystem
172 | .clone("#{fixtures_dir}/quick/gemirro-0.0.1.gemspec.rz",
173 | "#{indexer.directory}/quick/gemirro-0.0.1.gemspec.rz")
174 |
175 | allow(indexer).to receive(:make_temp_directories)
176 | allow(indexer).to receive(:update_specs_index)
177 | allow(indexer).to receive(:compress_indicies)
178 | allow(indexer).to receive(:compress_indices)
179 | allow(indexer).to receive(:build_zlib_file)
180 | # rubocop:disable Metrics/LineLength
181 | allow(indexer).to receive(:build_marshal_gemspecs).once.and_return(["#{indexer.directory}/quick/gemirro-0.0.1.gemspec.rz"])
182 | # rubocop:enable Metrics/LineLength
183 |
184 | allow(Marshal).to receive(:load).and_return(['content'])
185 | allow(Marshal).to receive(:dump).and_return(['content'])
186 | allow(FileUtils).to receive(:mv)
187 | allow(File).to receive(:utime)
188 |
189 | indexer.update_index
190 | end
191 | end
192 | end
193 |
--------------------------------------------------------------------------------
/spec/gemirro/mirror_directory_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/mirror_directory'
3 | require 'gemirro/mirror_file'
4 |
5 | # MirrorDirectory tests
6 | module Gemirro
7 | describe 'MirrorDirectory' do
8 | include FakeFS::SpecHelpers
9 |
10 | before(:each) do
11 | @mirror_directory = MirrorDirectory.new('./')
12 | end
13 |
14 | it 'should be initialized' do
15 | expect(@mirror_directory.path).to eq('./')
16 | end
17 |
18 | it 'should add directory' do
19 | expect(@mirror_directory.add_directory('test/test2'))
20 | .to be_a(MirrorDirectory)
21 | expect(File.directory?('./test/test2')).to be_truthy
22 | end
23 |
24 | it 'should add file' do
25 | result = @mirror_directory.add_file('file', 'content')
26 | expect(result).to be_a(MirrorFile)
27 | expect(result.read).to eq('content')
28 | end
29 |
30 | it 'should test if file exists' do
31 | expect(@mirror_directory.file_exists?('test')).to be_falsy
32 | @mirror_directory.add_file('test', 'content')
33 | expect(@mirror_directory.file_exists?('test')).to be_truthy
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/gemirro/mirror_file_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/mirror_file'
3 |
4 | # Mirror file tests
5 | module Gemirro
6 | describe 'MirrorFile' do
7 | include FakeFS::SpecHelpers
8 |
9 | before(:each) do
10 | @mirror_file = MirrorFile.new('./test')
11 | end
12 |
13 | it 'should be initialized' do
14 | expect(@mirror_file.path).to eq('./test')
15 | end
16 |
17 | it 'should write and read content' do
18 | expect(@mirror_file.write('content')).to be_nil
19 | expect(@mirror_file.read).to eq('content')
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/gemirro/server_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rack/test'
2 | require 'json'
3 | require 'parallel'
4 | require 'sinatra/base'
5 | require 'thin'
6 | require 'base64'
7 | require 'gemirro/utils'
8 | require 'gemirro/mirror_directory'
9 | require 'gemirro/mirror_file'
10 | require 'gemirro/gem_version_collection'
11 |
12 | ENV['RACK_ENV'] = 'test'
13 |
14 | # Rspec mixin module
15 | module RSpecMixin
16 | include Rack::Test::Methods
17 | def app
18 | require 'gemirro/server'
19 | Gemirro::Server
20 | end
21 | end
22 |
23 | RSpec.configure do |c|
24 | c.include RSpecMixin
25 | end
26 |
27 | # Server tests
28 | module Gemirro
29 | describe 'Gemirro::Server' do
30 | include FakeFS::SpecHelpers
31 |
32 | before(:each) do
33 | @fake_logger = Logger.new(STDOUT)
34 | MirrorDirectory.new('/var/www/gemirro').add_directory('gems')
35 | MirrorDirectory.new('/').add_directory('tmp')
36 | MirrorFile.new('/var/www/gemirro/test').write('content')
37 | Gemirro.configuration.destination = '/var/www/gemirro'
38 | Utils.instance_eval('@cache = nil')
39 | Utils.instance_eval('@gems_orig_collection = nil')
40 | Utils.instance_eval('@gems_source_collection = nil')
41 | FakeFS::FileSystem.clone(Gemirro::Configuration.views_directory)
42 | allow_any_instance_of(Indexer).to receive(:compress_indices)
43 | allow_any_instance_of(Indexer).to receive(:rand).and_return('0')
44 |
45 | source = Source.new('Rubygems', 'https://rubygems.org')
46 | allow(Gemirro.configuration).to receive(:source).and_return(source)
47 | end
48 |
49 | context 'HTML render' do
50 | it 'should display index page' do
51 | allow(Logger).to receive(:new).exactly(3).times.and_return(@fake_logger)
52 | allow(@fake_logger).to receive(:tap)
53 | .and_return(nil)
54 | .and_yield(@fake_logger)
55 |
56 |
57 | MirrorFile.new('/var/www/gemirro/versions.md5.aaa256.list').write('created_at: 2025-01-01T00:00:00Z\n---\nvolay 0.1.0\n')
58 |
59 | get '/'
60 | expect(last_response).to be_ok
61 | end
62 |
63 | it 'should return 404' do
64 | get '/wrong-path'
65 | expect(last_response.status).to eq(404)
66 | expect(last_response).to_not be_ok
67 | end
68 |
69 | it 'should return 404 when gem does not exist' do
70 | get '/gem/something'
71 | expect(last_response.status).to eq(404)
72 | expect(last_response).to_not be_ok
73 | end
74 |
75 | it 'should display gem specifications' do
76 | MirrorFile.new('/var/www/gemirro/versions.md5.aaa256.list').write(%(created_at: 2025-01-01T00:00:00Z\n---\nvolay 0.1.0 checksum))
77 |
78 | get '/gem/volay'
79 |
80 | expect(last_response.status).to eq(200)
81 | expect(last_response).to be_ok
82 | end
83 |
84 | it 'responds to compact_index /names' do
85 | MirrorFile.new('/var/www/gemirro/names.md5.aaa256.list').write(%(---\nvolay))
86 |
87 | get '/names'
88 | expect(last_response.status).to eq(200)
89 | expect(last_response).to be_ok
90 |
91 |
92 | expect(last_response.body).to eq(%(---\nvolay))
93 | expect(last_response.headers['etag']).to eq('"md5"')
94 | expect(last_response.headers['repr-digest']).to eq(%(sha-256=#{Base64.strict_encode64(['aaa256'].pack('H*'))}))
95 | end
96 |
97 | it 'responds to compact_index /info/[gemname]' do
98 | MirrorDirectory.new('/var/www/gemirro/info')
99 | MirrorFile.new('/var/www/gemirro/info/volay.md5.aaa256.list').write('---\n 0.1.0 |checksum:sha256\n')
100 |
101 |
102 | get '/info/volay'
103 | expect(last_response.status).to eq(200)
104 | expect(last_response).to be_ok
105 | expect(last_response.body).to eq('---\n 0.1.0 |checksum:sha256\n')
106 | expect(last_response.headers['etag']).to eq('"md5"')
107 | expect(last_response.headers['repr-digest']).to eq(%(sha-256=#{Base64.strict_encode64(['aaa256'].pack('H*'))}))
108 | end
109 |
110 |
111 | it 'responds to compact_index /versions' do
112 | MirrorFile.new('/var/www/gemirro/versions.md5.aaa256.list').write(%(created_at: 2025-01-01T00:00:00Z\n---\nvolay 0.1.0))
113 |
114 | get '/versions'
115 | expect(last_response.status).to eq(200)
116 | expect(last_response).to be_ok
117 | expect(last_response.body).to eq(%(created_at: 2025-01-01T00:00:00Z\n---\nvolay 0.1.0))
118 | expect(last_response.headers['etag']).to eq('"md5"')
119 | expect(last_response.headers['repr-digest']).to eq(%(sha-256=#{Base64.strict_encode64(['aaa256'].pack('H*'))}))
120 | end
121 |
122 |
123 | end
124 |
125 | context 'Download' do
126 | it 'should download existing file' do
127 | get '/test'
128 | expect(last_response.body).to eq('content')
129 | expect(last_response).to be_ok
130 | end
131 |
132 | it 'should try to download gems.' do
133 | source = Gemirro::Source.new('test', 'https://rubygems.org')
134 |
135 | versions_fetcher = Gemirro::VersionsFetcher.new(source)
136 | allow(versions_fetcher).to receive(:fetch).once.and_return(true)
137 |
138 | gems_fetcher = Gemirro::GemsFetcher.new(source, versions_fetcher)
139 | allow(gems_fetcher).to receive(:fetch).once.and_return(true)
140 | allow(gems_fetcher).to receive(:gem_exists?).once.and_return(true)
141 |
142 | Struct.new('GemIndexer')
143 | gem_indexer = Struct::GemIndexer.new
144 | allow(gem_indexer).to receive(:only_origin=).once.and_return(true)
145 | allow(gem_indexer).to receive(:ui=).once.and_return(true)
146 | allow(gem_indexer).to receive(:update_index).once.and_return(true)
147 |
148 | allow(Gemirro.configuration).to receive(:source)
149 | .twice.and_return(source)
150 | allow(Gemirro::GemsFetcher).to receive(:new)
151 | .once.and_return(gems_fetcher)
152 | allow(Gemirro::VersionsFetcher).to receive(:new)
153 | .once.and_return(versions_fetcher)
154 | allow(Gemirro::Indexer).to receive(:new).once.and_return(gem_indexer)
155 | allow(::Gem::SilentUI).to receive(:new).once.and_return(true)
156 |
157 | allow(Gemirro.configuration).to receive(:logger)
158 | .exactly(4).and_return(@fake_logger)
159 | allow(@fake_logger).to receive(:info).exactly(4)
160 |
161 | get '/gems/gemirro-0.0.1.gem'
162 | expect(last_response).to_not be_ok
163 | expect(last_response.status).to eq(404)
164 |
165 | MirrorFile.new('/var/www/gemirro/gems/gemirro-0.0.1.gem')
166 | .write('content')
167 | get '/gems/gemirro-0.0.1.gem'
168 | expect(last_response).to be_ok
169 | expect(last_response.status).to eq(200)
170 | expect(last_response.body).to eq('content')
171 | end
172 |
173 | it 'should catch exceptions' do
174 | source = Gemirro::Source.new('test', 'https://rubygems.org')
175 |
176 | versions_fetcher = Gemirro::VersionsFetcher.new(source)
177 | allow(versions_fetcher).to receive(:fetch).once.and_return(true)
178 |
179 | gems_fetcher = Gemirro::VersionsFetcher.new(source)
180 | allow(gems_fetcher).to receive(:fetch)
181 | .once.and_raise(StandardError, 'Not ok')
182 |
183 | gem_indexer = Struct::GemIndexer.new
184 | allow(gem_indexer).to receive(:only_origin=).once.and_return(true)
185 | allow(gem_indexer).to receive(:ui=).once.and_return(true)
186 | allow(gem_indexer).to receive(:update_index)
187 | .once.and_raise(SystemExit)
188 |
189 | allow(Gemirro.configuration).to receive(:source)
190 | .twice.and_return(source)
191 | allow(Gemirro::GemsFetcher).to receive(:new)
192 | .once.and_return(gems_fetcher)
193 | allow(Gemirro::VersionsFetcher).to receive(:new)
194 | .once.and_return(versions_fetcher)
195 | allow(Gemirro::Indexer).to receive(:new).once.and_return(gem_indexer)
196 | allow(::Gem::SilentUI).to receive(:new).once.and_return(true)
197 |
198 | allow(Gemirro.configuration).to receive(:logger)
199 | .exactly(4).and_return(@fake_logger)
200 | allow(@fake_logger).to receive(:info).exactly(3)
201 | allow(@fake_logger).to receive(:error)
202 | get '/gems/gemirro-0.0.1.gem'
203 | expect(last_response).to_not be_ok
204 | end
205 | end
206 |
207 | context 'dependencies' do
208 | it 'should retrieve nothing and give 404' do
209 | get '/api/v1/dependencies'
210 | expect(last_response.status).to eq(404)
211 | expect(last_response).to_not be_ok
212 | end
213 |
214 |
215 | it 'should retrieve nothing and give 404' do
216 | get '/api/v1/dependencies.json?gems=gemirro'
217 | expect(last_response.status).to eq(404)
218 | expect(last_response).to_not be_ok
219 | end
220 |
221 | it 'should retrieve nothing and give 404' do
222 | MirrorDirectory.new('/var/www/gemirro')
223 | .add_directory('quick/Marshal.4.8')
224 | # rubocop:disable Metrics/LineLength
225 | MirrorFile.new('/var/www/gemirro/quick/Marshal.4.8/' \
226 | 'volay-0.1.0.gemspec.rz')
227 | .write("x\x9C\x8D\x94]\x8F\xD2@\x14\x86\x89Y\xBB\xB4|\xEC\x12\xD7h" \
228 | "\xD4h\xD3K\x13J\x01\x97\xC84n\x9A\xA8\xBBi\xE2\xC5\x06\xBB" \
229 | "{\xC3\x85)\xE5\x00\x13f:u:E\xD1\xC4\xDF\xE6\xB5\xBF\xCAiK" \
230 | "\x11\xE3GK\xEF\x98\xF7\xBC\xCFy\xCF\xC9\xCCQ=A\x0F\xAE\x80" \
231 | "\"\xF4>\x82\x00/p\xE0\v\xCC\xC2;\xC1\xDD\xA3\xFA\xF4\xA1k4" \
232 | "\x06\xA6e\xF6_(Hy\xEBa\xD55\xB4\r#\xFEV\xB1k\xDE\r\xEAdu" \
233 | "\xB7\xC0cY1U\xE4\xA1\x95\x8A\xD3C7A\xAA\x87)\xB4\x9C\x1FO" \
234 | "\xBE\xD7\xE4OA\xEA\x17\x16\x82k\xD4o\xBC\xD7\x99\xC2x\xEC" \
235 | "\xAD@\xBFe$\xA1\xA0\xC7\xDBX\x00\xD5\x05/\xBC\xEFg\xDE\x13" \
236 | "\xF8\x98`\x0E\x14B1U\xE4w\xEC\x1A\xC7\x17\xAF2\x85\xADd\xC4" \
237 | "\xBE96\x87\xF9\x1F\xEA\xDF%\x8A\x95\xE3T\x9E\xCC2\xF3i\x9B" \
238 | "\xA1\xB3\xCC\xFE\rD\x10\xCE!\f\xB6\x1A\xD2\x9C\xD0\xA7\xB2" \
239 | "\xBF\x13\x8A?\x13<\xEB\x06\x04\xA7b\xD4q\xF8\xAF&\x0E!\xDF" \
240 | ".~\xEF\xE3\xDC\xCC@\xD2Hl\#@M\x9E\x84BN\x00\x9D:\x11\a\x0E" \
241 | "\x04\xFC\x18.\xD1#g\x93\xCF\xEB\xC3\x81m\\\xC1\x97\xD9" \
242 | "\x9Af7\\\xE3l\xD7_\xBC\x02BX\"\xD23\xBB\xF9o\x83A\xB1\x12" \
243 | "\xBBe\xB7\xED\x93K\xFB\xB4\x82\xB6\x80\xA9K\xB1\x1E\x96" \
244 | "\x10\xEA\x03sP\xCD\xBFP\x16\xEE\x8D\x85\xBF\x86E\\\x96" \
245 | "\xC02G\xF9\b\xEC\x16:\x9D\xC3\x06\b\x8B\xD2\xA9\x95\x84" \
246 | "\xD9\x97\xED\xC3p\x89+\x81\xA9}\xAB`\xD9\x9D\xFF\x03\xF6" \
247 | "\xD2\xC2\xBF\xCD\xFD`\xDD\x15\x10\x97\xED\xA4.[\xAB\xC6(" \
248 | "\x94\x05B\xE3\xB1\xBC\xA5e\xF6\xC3\xAA\x11\n\xE5>A\x8CiD " \
249 | "`\x9B\xF2\x04\xE3\xCA\t\xC6\x87\by-f,`Q\xD9\x1E,sp^q\x0F" \
250 | "\x85\xD4r\x8Dg\x11\x06\xCE\xC1\xE4>\x9D\xF9\xC9\xFC\xE5" \
251 | "\xC8YR\x1F\x133`4\xBB\xF9R~\xEF:\x93\xE8\x93\\\x92\xBF\r" \
252 | "\xA3\t\xF8\x84l\xF5<\xBF\xBE\xF9\xE3Q\xD2?q,\x04\x84:\x0E" \
253 | "\xF5\xF4\x1D1\xF3\xBA\xE7+!\"\xD4\xEB-\xB1X%\xB3\x14\xD3" \
254 | "\xCB\xEDw\xEE\xBD\xFDk\xE99OSz\xF3\xEA\xFA]w7\xF5\xAF\xB5" \
255 | "\x9F+\xFEG\x96")
256 | # rubocop:enable Metrics/LineLength
257 |
258 | MirrorFile.new('/var/www/gemirro/api/v1/dependencies/volay.md5.sha.list')
259 | .write(Marshal.dump([
260 | {
261 | name: 'volay',
262 | number: "0.1.0",
263 | platform: 'ruby',
264 | dependencies: [
265 | {
266 | name: 'json',
267 | requirement: '~> 2.1'
268 | }
269 | ]
270 | }
271 | ]))
272 |
273 | gem = Gemirro::GemVersion.new('volay', '0.1.0', 'ruby')
274 | collection = Gemirro::GemVersionCollection.new([gem])
275 | allow(Utils).to receive(:gems_collection)
276 | .and_return(collection)
277 | get '/api/v1/dependencies.json?gems=volay'
278 | expect(last_response.status).to eq(404)
279 | expect(last_response).to_not be_ok
280 | end
281 | end
282 | end
283 | end
284 |
--------------------------------------------------------------------------------
/spec/gemirro/source_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/http'
3 | require 'gemirro/utils'
4 | require 'gemirro/source'
5 |
6 | # Source tests
7 | module Gemirro
8 | describe 'Source' do
9 | before(:each) do
10 | @source = Source.new('RubyGems', 'https://rubygems.org')
11 | allow(Utils.logger).to receive(:info)
12 | end
13 |
14 | it 'should be initialized' do
15 | expect(@source.name).to eq('rubygems')
16 | expect(@source.host).to eq('https://rubygems.org')
17 | expect(@source.gems).to eq([])
18 | end
19 |
20 | it 'should fetch versions' do
21 | Struct.new('FetchVersions', :body)
22 | result = Struct::FetchVersions.new(true)
23 | allow(Http).to receive(:get).once.with(
24 | "https://rubygems.org/versions"
25 | ).and_return(result)
26 | expect(@source.fetch_versions).to be_truthy
27 | end
28 |
29 | it 'should fetch gem' do
30 | Struct.new('FetchGem', :body)
31 | result = Struct::FetchGem.new(true)
32 | allow(Http).to receive(:get).once
33 | .with('https://rubygems.org/gems/gemirro-0.0.1.gem').and_return(result)
34 | expect(@source.fetch_gem('gemirro-0.0.1.gem')).to be_truthy
35 | end
36 |
37 | it 'should fetch gemspec' do
38 | Struct.new('FetchGemspec', :body)
39 | result = Struct::FetchGemspec.new(true)
40 | allow(Http).to receive(:get).once
41 | .with('https://rubygems.org/quick/Marshal.4.8/gemirro-0.0.1.gemspec.rz').and_return(result)
42 | expect(@source.fetch_gemspec('gemirro-0.0.1.gemspec.rz')).to be_truthy
43 | end
44 |
45 | it 'should add gems' do
46 | expect(@source.gems).to eq([])
47 | @source.gem('gemirro')
48 | result = @source.gems
49 | expect(result[0].name).to eq('gemirro')
50 | expect(result[0].requirement).to be_a(::Gem::Requirement)
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/gemirro/versions_fetcher_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'gemirro/versions_fetcher'
3 |
4 | # VersionsFetcher tests
5 | module Gemirro
6 | describe 'VersionsFetcher' do
7 | include FakeFS::SpecHelpers
8 |
9 | before(:each) do
10 | @source = Source.new('RubyGems', 'https://rubygems.org')
11 | @fetcher = VersionsFetcher.new(@source)
12 | end
13 |
14 | it 'should be initialized' do
15 | expect(@fetcher.source).to be(@source)
16 | end
17 |
18 | it 'should fetch versions' do
19 | allow(@source).to receive(:fetch_versions).once.and_return([])
20 | allow(VersionsFile).to receive(:load).with('nothing')
21 | allow(File).to receive(:write).once
22 | allow(File).to receive(:read).once.and_return('nothing')
23 | expect(@fetcher.fetch).to be_nil
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/gemirro/versions_file_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'zlib'
3 | require 'gemirro/versions_file'
4 |
5 | # VersionsFile tests
6 | module Gemirro
7 | describe 'VersionsFile' do
8 | include FakeFS::SpecHelpers
9 |
10 | it 'should load versions file' do
11 | spec = %(created_at: 2025-01-01T00:00:00Z\n---\ngemirro 0.0.1.alpha1,0.0.1,0.0.2.alpha2,0.0.2 checksum)
12 |
13 | result = VersionsFile.new(spec)
14 | expect(result).to be_a(VersionsFile)
15 |
16 | expect(result.versions_string).to eq(%(created_at: 2025-01-01T00:00:00Z\n---\ngemirro 0.0.1.alpha1,0.0.1,0.0.2.alpha2,0.0.2 checksum))
17 |
18 | expect(result.versions_hash).to eq(
19 | 'gemirro' => [
20 | ['gemirro', ::Gem::Version.new('0.0.1.alpha1'), 'ruby'],
21 | ['gemirro', ::Gem::Version.new('0.0.1'), 'ruby'],
22 | ['gemirro', ::Gem::Version.new('0.0.2.alpha2'), 'ruby'],
23 | ['gemirro', ::Gem::Version.new('0.0.2'), 'ruby']
24 | ]
25 | )
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH << File.expand_path('../../lib', __FILE__)
2 |
3 | require 'simplecov'
4 | require 'confstruct'
5 | require 'logger'
6 | require 'fakefs/spec_helpers'
7 |
8 | SimpleCov.start do
9 | add_filter '/spec/'
10 | end
11 |
--------------------------------------------------------------------------------
/task/manifest.rake:
--------------------------------------------------------------------------------
1 | desc 'Generates the MANIFEST file'
2 | task :manifest do
3 | files = `git ls-files`.split("\n").sort
4 | handle = File.open(File.expand_path('../../MANIFEST', __FILE__), 'w')
5 |
6 | handle.write(files.join("\n"))
7 | handle.close
8 | end
9 |
--------------------------------------------------------------------------------
/task/rspec.rake:
--------------------------------------------------------------------------------
1 | require 'rspec/core/rake_task'
2 |
3 | desc 'Run Rspec tests'
4 | RSpec::Core::RakeTask.new(:spec) do |t|
5 | t.rspec_opts = '--color --format documentation --backtrace'
6 | end
7 |
--------------------------------------------------------------------------------
/task/rubocop.rake:
--------------------------------------------------------------------------------
1 | require 'rubocop/rake_task'
2 |
3 | desc 'Run RuboCop'
4 | RuboCop::RakeTask.new(:rubocop)
5 |
--------------------------------------------------------------------------------
/template/config.rb:
--------------------------------------------------------------------------------
1 | # This is the main configuration file for your RubyGems mirror. Here you can
2 | # change settings such as the location to store Gem files in and what source
3 | # and Gems you'd like to mirror at start.
4 | Gemirro.configuration.configure do
5 | # Define sinatra environment
6 | environment :production
7 |
8 | # The directory to store indexing information as well as the Gem files in.
9 | destination File.expand_path('../public', __FILE__)
10 |
11 | # If you're in development mode your probably want to switch to a debug
12 | # logging level.
13 | logger.level = Logger::INFO
14 |
15 | # If you want to run your server on a specific host and port, you must
16 | # change the following parameters (server_host and server_port).
17 | #
18 | # server.host 'localhost'
19 | # server.port '2000'
20 |
21 | # If you don't want the server to run daemonized, uncomment the following
22 | # server.daemonize false
23 | server.access_log File.expand_path('../logs/access.log', __FILE__)
24 | server.error_log File.expand_path('../logs/error.log', __FILE__)
25 |
26 | # Number of parallel processes while indexing. Too many will kill
27 | # your indexing process prematurely.
28 | #
29 | # update_threads Etc.nprocessors - 1
30 | # update_threads 4
31 |
32 | # If you don't want to generate indexes after each fetched gem.
33 | #
34 | # update_on_fetch false
35 |
36 | # If you don't want to fetch gem if file does not exists when
37 | # running gemirro server.
38 | #
39 | # fetch_gem false
40 |
41 | # If upstream repository requires authentication
42 | # upstream_user 'username'
43 | # upstream_password 'password'
44 | # upstream_domain 'https://internal.com'
45 |
46 | # Enforce the the base_auth
47 | # basic_auth true
48 |
49 | # Set the proxy server if behind the firewall
50 | # proxy 'http://proxy.internal.com:80'
51 |
52 | # Root CA cert location if additional root ca is added
53 | # This will overwrite verfiy_mode. use PEER as default
54 | # rootca '/etc/root_ca.crt'
55 |
56 | # Not verify certificate in case the proxy has self-signed cert
57 | # verify_mode false
58 |
59 | # You must define a source which where gems will be downloaded.
60 | # All gem in the block will be downloaded with the update command.
61 | # Other gems will be downloaded with the server.
62 | define_source 'rubygems', 'https://rubygems.org' do
63 | gem 'rack', '>= 3.0.0'
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/template/logs/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/template/public/dist/css/gemirro.css:
--------------------------------------------------------------------------------
1 | body { max-width: 620px; margin: 0 auto; padding: 0; font-family: sans-serif; }
2 |
3 | dl { display: grid; grid-template-columns: 1fr min-content; }
4 |
5 | dt { grid-column: 1; margin: 0; padding: 8px 0; border-bottom: 1px solid #e2e2e2; }
6 | dd { grid-column: 2; margin: 0; padding: 8px 0; border-bottom: 1px solid #e2e2e2; }
7 |
8 | dd.full { grid-column: 1/2; border-bottom: 0; }
9 | dd.description, dd.authors, dd.dependencies { grid-column: 1/2; border-bottom: 0; }
10 |
11 |
12 | a, a:visited, a:active, a:hover { color: #007bff; }
13 | .btn { color: #fff; background: #007bff; padding: 5px; border-radius: 4px; text-decoration: none; }
14 | .btn:visited, .btn:active, .btn:hover { color: #fff; }
15 |
16 |
17 | ul { list-style-type: none; margin: 0; padding: 0 0 0 1em; }
18 |
19 |
20 | @media (prefers-color-scheme: dark) {
21 | html, body { background: #16161d; color: #fff; }
22 | a, a:visited, a:active, a:hover { color: lightskyblue; }
23 | .btn { color: #16161d; background: lightskyblue; padding: 5px; border-radius: 4px; text-decoration: none; }
24 | .btn:visited, .btn:active, .btn:hover { color: #16161d; }
25 | }
26 |
--------------------------------------------------------------------------------
/template/public/gems/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PierreRambaud/gemirro/b5ffe34c99daadb1f4bdddb5d354cf51043a9088/template/public/gems/.gitkeep
--------------------------------------------------------------------------------
/views/gem.erb:
--------------------------------------------------------------------------------
1 |
2 | Back to Gem Index
3 |
4 |
5 |
6 |
7 |
8 | <% gem.by_name do |name, versions| %>
9 |