├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── cert
└── ca-chain.cert.pem
├── cmd
├── branch.go
├── branchActive.go
├── branchActiveGet.go
├── branchActiveSet.go
├── branchCreate.go
├── branchList.go
├── branchRemove.go
├── branchRevert.go
├── branchUpdate.go
├── commit.go
├── dio_test.go
├── info.go
├── licence.go
├── licenceAdd.go
├── licenceGet.go
├── licenceList.go
├── licenceRemove.go
├── list.go
├── log.go
├── pull.go
├── push.go
├── release.go
├── releaseCreate.go
├── releaseList.go
├── releaseRemove.go
├── root.go
├── select.go
├── shared.go
├── status.go
├── tag.go
├── tagCreate.go
├── tagList.go
├── tagRemove.go
├── types.go
└── version.go
├── config
└── config.toml
├── dio.go
├── go.mod
├── go.sum
├── misc
├── .gitignore
└── build_binaries.sh
└── test_data
├── 19kB.sqlite
├── ca-chain-docker.cert.pem
├── default.cert.pem
├── docker-dev.dbhub.io.cert.pem
└── docker-dev.dbhub.io.key.pem
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Docker Image CI
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 |
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 |
17 | - name: Install NodeJS 20
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 20
21 |
22 | # Build and start the DBHub.io server daemons
23 | - name: Checkout the DBHub.io source code
24 | uses: actions/checkout@v4
25 | with:
26 | repository: 'sqlitebrowser/dbhub.io'
27 | path: daemons
28 |
29 | - name: Build the DBHub.io daemons
30 | run: cd daemons; yarn docker:build
31 |
32 | - name: Update the daemon config file
33 | run: cd daemons; sed -i 's/bind_address = ":9443"/bind_address = "0.0.0.0:9443"/' docker/config.toml
34 |
35 | - name: Start the DBHub.io daemons
36 | run: cd daemons; docker run -itd --rm --name dbhub-build --net host dbhub-build:latest && sleep 5
37 |
38 | # Build and test dio
39 | - name: Checkout dio source code
40 | uses: actions/checkout@v4
41 | with:
42 | path: main
43 |
44 | - name: Set up Go
45 | uses: actions/setup-go@v4
46 | with:
47 | go-version: '1.21'
48 |
49 | - name: Build dio
50 | run: cd main; go build -v
51 |
52 | - name: Test dio
53 | run: cd main; IS_TESTING=yes go test ./cmd -v -check.v
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.dll
4 | *.so
5 | *.dylib
6 |
7 | # Test binary, build with `go test -c`
8 | *.test
9 |
10 | # Output of the go coverage tool, specifically when used with LiteIDE
11 | *.out
12 |
13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
14 | .glide/
15 |
16 | # Exclude Goland project files
17 | .idea
18 |
19 | # dio binary
20 | dio
21 |
22 | # Exclude database, metadata and licence files used in testing
23 | *.db
24 | .dio
25 | *-LICENCE
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dio
2 |
3 | Dio is our reference command line interface (CLI) application for working with [DBHub.io](https://dbhub.io/).
4 |
5 | It can be used used to:
6 |
7 | * transfer databases to and from the cloud (pushing and pulling)
8 | * check their version history
9 | * create branches, tags, releases, and commits
10 | * diff changes (in a future release)
11 | * and more... (eventually)
12 |
13 | It's at a fairly early stage in its development, though the main pieces should
14 | all work. It certainly needs more polish to be more user-friendly though.
15 |
16 | ## Building from source
17 |
18 | Dio requires Go to be installed (version 1.17+ is known to work). Building should
19 | just require:
20 |
21 | ```bash
22 | $ go get github.com/sqlitebrowser/dio
23 | $ go install github.com/sqlitebrowser/dio
24 | ```
25 |
26 | ## Getting Started
27 |
28 | To use it, do the following:
29 | 1. Create a folder named `.dio` in your home directory;
30 | ```bash
31 | $ cd ~
32 | $ mkdir .dio
33 | ```
34 | 2. Download [`ca-chain-cert.pem`](https://github.com/sqlitebrowser/dio/blob/master/cert/ca-chain.cert.pem) to `~/.dio/`. For example:
35 | ```bash
36 | $ cd ~/.dio
37 | $ wget https://github.com/sqlitebrowser/dio/raw/master/cert/ca-chain.cert.pem
38 | ```
39 | 3. Generate a certificate file for yourself at [DBHub.io](https://dbhub.io/) and save it in `~/.dio/`.
40 | 4. Create the following text file, and name it `~/.dio/config.toml`:
41 | ```toml
42 | [user]
43 | name = "Your Name"
44 | email = "youremail@example.org"
45 |
46 | [certs]
47 | cachain = "/home/username/.dio/ca-chain.cert.pem"
48 | cert = "/home/username/.dio/username.cert.pem"
49 |
50 | [general]
51 | cloud = "https://db4s.dbhub.io"
52 |
53 | ```
54 | 5. Change the `name` and `email` values to your name and email address
55 | 6. Change `/home/username` to the path to your home directory
56 | 7. Make sure `cachain` points to the downloaded ca-chain.cert.pem file
57 | 8. Make sure `cert` points to your generated DBHub.io certificate
58 | * Leave the `cloud` value pointing to https://db4s.dbhub.io
59 |
60 | To verify this file is set up correctly, type:
61 | ```bash
62 | $ dio info
63 | ```
64 | which will display the information loaded from this configuration file.
65 |
66 | Dio has a `help` option (`dio help`) which is useful for listing the available dio
67 | commands, explaining their purpose, etc.
68 |
--------------------------------------------------------------------------------
/cert/ca-chain.cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIF8DCCA9igAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAkdC
3 | MRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhEQkh1Yi5pbzEnMCUGA1UECwwe
4 | REJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSUwIwYDVQQDDBxEQkh1Yi5p
5 | byBERVZFTE9QTUVOVCBSb290IENBMB4XDTE3MDMwNDEwNTI0NloXDTI3MDMwNTEw
6 | NTI0NlowgYoxCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQK
7 | DAhEQkh1Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9y
8 | aXR5MS0wKwYDVQQDDCREQkh1Yi5pbyBERVZFTE9QTUVOVCBJbnRlcm1lZGlhdGUg
9 | Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKOkhcRjOBkxQdzDie
10 | S6dfXIiQ9Qk2iCqfBVGPCFMjNrzUywjLvi0ZVbo7CaR4gveaXwcH94C8sxaqmm+7
11 | SB3FimcgRfq1fwPS78wKyeAtb37TGu+Xwu/+4l320BwdmaCLx10kjT2pOf29t7MH
12 | qBLF3p4+7Jza5IPL3Ddq4O8iyUEvd3QfHZ2RgzY1M2APezG51DLRUHX7s5d8Bbe8
13 | HcHmsrWCbJpbPzGCj2C4UiaT5ZCMPLFW+pbUnnZemVriDHekPBSNgo8AdSnnUypm
14 | YgdidlUFdZ8LAMqJYouY6On32+huXWK8QXnWz/TuGnP2mV4KgVPIkM89gymLakAL
15 | x8UX44csekodLsq4xmHnBZq9eDddjRKK/A6iG95GqYW4g3QbnjU56UCe4NlzRRj1
16 | jATjLvJ3KHG45YyW8qjlr3vvVDNLiYPUs3ZDo4N8qChYFKbGKUljay6JpHjek16g
17 | 6Lmy6+8NUopKh+Q7vIH2LLpgh5xNlCRvHHOpE7TR2XtmCOvp2FHCSUqym+inOuvN
18 | CoSwVpooi5y4yvnVmSa9gxAp5AoptGzo/ucEsRvvf7giu7JnWIImYv1xvFOnqN2G
19 | GxRpXujAvrgpH79zdaj3VNUNebUw9PLeEksj4SuoAt3CxPrPnv+QecMXzRjLCgS4
20 | PAygRceqJfsUl/dRhZTpoZr5ywIDAQABo2YwZDAdBgNVHQ4EFgQUzl/NZNsCDfwl
21 | RFDAZBt/2KfiH+UwHwYDVR0jBBgwFoAUh2SO9T3EiSMToeLLKQLwYRsIryUwEgYD
22 | VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD
23 | ggIBABKx6uDYuvYPrJtfyNpW63ELWukVVNMHojipg4M2dlV+phgooRGqJwE1Nx8s
24 | dQ3lDIIpIqi7eVfkm2SSYsMN3AUHhLnX9VeyWe4ffKs2mHCaQ6nIk+niE01zZC4z
25 | bGHNZwJNkgKa8s2E+iK1Z/QB1QicS9PiQoJHHkLbnS7v0YqowdXgMniU1yqIjmf7
26 | aWhK0Gt51iXFgVcz0lXHsJkdgl85JW6nN/EB1rtZ+tWDgfBpPL8JObVmr1qMUsNe
27 | nWaAf91DA+3DDWVCCMdtgvTIRc7srjYl7rpBW36Ztijm8fTvEWdB6zVT4BUl4lh9
28 | mECVV/Gx39oMyCMLq6X0jQ49tAuTjlCYRtQ1vRhKpfO+hJma01WBKPSrtJ3Yiiyk
29 | DFyi3Rs0w8GDN5FXRTfachExt5A1DAsUnxFz3JkdEJnmEgzzipa+FDAOC1JZvgxP
30 | I0GCJaQT60YuSyUsM+IPuedy2izRUImFLWXocn9y0Kbsqn7GY8pUYn4cXO7s2X/2
31 | PsOQyFHLmAiTrhTISO/58NUzLsudMY9d1V5ymVHXYBDwgRMMVoTQqniU3ArCIRWZ
32 | XgqjSf6psZaXS4/9wf4y6c+/WgkfAMbCGGj7mKLu2a9Lo5zfdCB73ZhEATJbMFwk
33 | KTYOLQO4zJEK2h+hmyqMaKv7EDB0BjLmbnFCcuvWL27aB5FL
34 | -----END CERTIFICATE-----
35 | -----BEGIN CERTIFICATE-----
36 | MIIF7DCCA9SgAwIBAgIJAKtqE1Cz9EmfMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD
37 | VQQGEwJHQjEQMA4GA1UECAwHRW5nbGFuZDERMA8GA1UECgwIREJIdWIuaW8xJzAl
38 | BgNVBAsMHkRCSHViLmlvIENlcnRpZmljYXRlIEF1dGhvcml0eTElMCMGA1UEAwwc
39 | REJIdWIuaW8gREVWRUxPUE1FTlQgUm9vdCBDQTAeFw0xNzAzMDQxMDE1MzBaFw0z
40 | NzAzMDQxMDE1MzBaMIGCMQswCQYDVQQGEwJHQjEQMA4GA1UECAwHRW5nbGFuZDER
41 | MA8GA1UECgwIREJIdWIuaW8xJzAlBgNVBAsMHkRCSHViLmlvIENlcnRpZmljYXRl
42 | IEF1dGhvcml0eTElMCMGA1UEAwwcREJIdWIuaW8gREVWRUxPUE1FTlQgUm9vdCBD
43 | QTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMlg1CFBIRwv87u2A8Gl
44 | IqIJgGBlEIF9EREF6UyIylHuvwEV7efOUwwaAoF2N1V4w7MlHPeW7o7eKMt0LFI/
45 | fDBRb6xz3bxnR8Mxr9p4zn77qocDU/AJDAk/ZMMRi4urIQg6tFBp1gsbSaWgsVFm
46 | wgewTyXSUu51PAgRtXPhiiMKwabjOjxZJGZY7vCP1vl6bL5Dp9pvoShSD/BcCB1a
47 | b2FiPBSTfl45Ovs+oW7enKOik/jKXqqGMtDCyjLl71wObTyn3Sv8uKuuMc0bY7ui
48 | 747sNmUWQFwP27BsXtHY27Q1dgC7oR1o3uyE9nLlOHrycLoVz/WuS5+UwbVlH+9x
49 | ixxuW8fhAHXO1hUG8ZNsUVqiBKaVryMsWgM76kCiRRHbwLXsSKu/zwo+HcHhnqku
50 | tq/+ibV9R0u1reSZ46rVhmLCuD1BWO5OEQRujlpGBAQPu0ajm7Ym+vG3MHTOeI3p
51 | LqAM+0QnLKhDDC6kwVpFmZkcvTsKBtIFRw26H6pGXmapzxrcuAzM6MKIQGiJaduf
52 | Vn8RvrTzSxGqWHb10DLAosfV2dBAT7qUpGw9yAtpjpKudjuZ6WDcAGUrPwozxfqo
53 | cWjamTMML8r2Lsm/DBtmMHcpxqLw17FzWSzkP8HVvLMJYkkkO6uixdUMnSTm5H7V
54 | VzZZSpbtRtcrVMFR9sqB+y41AgMBAAGjYzBhMB0GA1UdDgQWBBSHZI71PcSJIxOh
55 | 4sspAvBhGwivJTAfBgNVHSMEGDAWgBSHZI71PcSJIxOh4sspAvBhGwivJTAPBgNV
56 | HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA
57 | Uxjm+WdsvoQwZFQfcYAJDB4hLKe2257jBcfRJdC8kuU2hlNKLwJIoepd5ByMtFMz
58 | pYkcHvOVYPiYXBT+b6ReluvnNun3bELWa+XvhYdDIkgEqYnnIhnVKKjONOQOo1mh
59 | pvuA5iW7G9zxYHrdu3WFCPTIQU/YdRnN1X9uqS8AdPWIsuOqHl6+mivjSAgLDq1J
60 | 5mYatCKRIC1OxZDPqXRgyd0LwD9sU0ImBb/icLDa0bAWt/gXid75rZV08zOOzd8f
61 | 4CmDO679o7D0S4opf3JSpyjWg5ALncmygcX83Uk6AaNvlEKvwKTp5vCNsiHml7IO
62 | KuPXRqJJxAFErpjbaboon+WX3zpOh0w4DRS6UB7mmWeZ8/rbSG5KyHUvCy6gAEoW
63 | i1EKfrt0T+7xx2jERdSTX0Vy3+G5CrS74MRgdDz+QYFVevY1zGc34U/+3l32VtMZ
64 | x7I5jLXb+LiZ5JQrzfPf62xO9jJogczt9DuHQ4BOAtqAFTzZvMRnyCgxD49IfsZ6
65 | YZ/CNNlqdoPJkXmR/BHiUk/k6ZC3vYa/Am3tniQp7RxeV6s4hTB8XS6CN2Aq8cnS
66 | a5dtZbFSXNMB4QfBKGDg/gUwxU3j6VyoQJnmKt2QVgQqG6Sz1Px+YfmQXOMoGiFM
67 | IhGSBh8DEnh/aNtGXBF3OaYjAfSg9zfq9ATUmSzjzxY=
68 | -----END CERTIFICATE-----
69 |
--------------------------------------------------------------------------------
/cmd/branch.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | // branchCmd represents the branch command
8 | var branchCmd = &cobra.Command{
9 | Use: "branch",
10 | Short: "Work with branches for a database",
11 | }
12 |
13 | func init() {
14 | RootCmd.AddCommand(branchCmd)
15 | }
16 |
--------------------------------------------------------------------------------
/cmd/branchActive.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | var branchActiveCmd = &cobra.Command{
8 | Use: "active",
9 | Short: "Get and set the active branch for a database",
10 | }
11 |
12 | func init() {
13 | branchCmd.AddCommand(branchActiveCmd)
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/branchActiveGet.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | // Returns the name of the active branch for a database
11 | var branchActiveGetCmd = &cobra.Command{
12 | Use: "get [database name]",
13 | Short: "Get the active branch name for a database",
14 | RunE: func(cmd *cobra.Command, args []string) error {
15 | return branchActiveGet(args)
16 | },
17 | }
18 |
19 | func init() {
20 | branchActiveCmd.AddCommand(branchActiveGetCmd)
21 | }
22 |
23 | func branchActiveGet(args []string) error {
24 | // Ensure a database file was given
25 | var db string
26 | var err error
27 | var meta metaData
28 | if len(args) == 0 {
29 | db, err = getDefaultDatabase()
30 | if err != nil {
31 | return err
32 | }
33 | if db == "" {
34 | // No database name was given on the command line, and we don't have a default database selected
35 | return errors.New("No database file specified")
36 | }
37 | } else {
38 | db = args[0]
39 | }
40 | if len(args) > 1 {
41 | return errors.New("Only one database can be worked with at a time (for now)")
42 | }
43 |
44 | // Load the local metadata cache, without retrieving updated metadata from the cloud
45 | meta, err = localFetchMetadata(db, false)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | _, err = fmt.Fprintf(fOut, "Active branch: %s\n", meta.ActiveBranch)
51 | return err
52 | }
53 |
--------------------------------------------------------------------------------
/cmd/branchActiveSet.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "time"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var (
15 | branchActiveSetBranch string
16 | branchActiveSetForce *bool
17 | )
18 |
19 | // Sets the active branch for a database
20 | var branchActiveSetCmd = &cobra.Command{
21 | Use: "set [database name] --branch xxx",
22 | Short: "Set the active branch for a database",
23 | RunE: func(cmd *cobra.Command, args []string) error {
24 | return branchActiveSet(args)
25 | },
26 | }
27 |
28 | func init() {
29 | branchActiveCmd.AddCommand(branchActiveSetCmd)
30 | branchActiveSetCmd.Flags().StringVar(&branchActiveSetBranch, "branch", "",
31 | "Remote branch to set as active")
32 | branchActiveSetForce = branchActiveSetCmd.Flags().BoolP("force", "f", false,
33 | "Overwrite unsaved changes to the database?")
34 | }
35 |
36 | func branchActiveSet(args []string) error {
37 | // Ensure a database file was given
38 | var db string
39 | var err error
40 | var meta metaData
41 | if len(args) == 0 {
42 | db, err = getDefaultDatabase()
43 | if err != nil {
44 | return err
45 | }
46 | if db == "" {
47 | // No database name was given on the command line, and we don't have a default database selected
48 | return errors.New("No database file specified")
49 | }
50 | } else {
51 | db = args[0]
52 | }
53 | if len(args) > 1 {
54 | return errors.New("Only one database can be changed at a time (for now)")
55 | }
56 |
57 | // Ensure a branch name was given
58 | if branchActiveSetBranch == "" {
59 | return errors.New("No branch name given")
60 | }
61 |
62 | // If there's no local metadata cache, then create one
63 | meta, err = loadMetadata(db)
64 | if err != nil {
65 | return err
66 | }
67 |
68 | // Make sure the given branch name exists
69 | head, ok := meta.Branches[branchActiveSetBranch]
70 | if ok == false {
71 | return errors.New("That branch name doesn't exist for this database")
72 | }
73 |
74 | // Unless --force is specified, check whether the file has changed since the last commit, and let the user know
75 | if *branchActiveSetForce == false {
76 | changed, err := dbChanged(db, meta)
77 | if err != nil {
78 | return err
79 | }
80 | if changed {
81 | _, err = fmt.Fprintf(fOut, "%s has been changed since the last commit. Use --force if you really want to "+
82 | "overwrite it\n", db)
83 | return err
84 | }
85 | }
86 |
87 | // Get the details of the head commit for the target branch
88 | commit, ok := meta.Commits[head.Commit]
89 | if ok == false {
90 | return errors.New("Something has gone wrong. Head commit for the branch isn't in the commit list")
91 | }
92 | shaSum := commit.Tree.Entries[0].Sha256
93 | lastMod := commit.Tree.Entries[0].LastModified
94 |
95 | // Make sure the correct database from the target branch is in local cache
96 | err = checkDBCache(db, shaSum)
97 | if err != nil {
98 | return err
99 | }
100 |
101 | // Copy the database from local cache, so it matches the new branch head commit
102 | var b []byte
103 | b, err = ioutil.ReadFile(filepath.Join(".dio", db, "db", shaSum))
104 | if err != nil {
105 | return err
106 | }
107 | err = ioutil.WriteFile(db, b, 0644)
108 | if err != nil {
109 | return err
110 | }
111 | err = os.Chtimes(db, time.Now(), lastMod)
112 | if err != nil {
113 | return err
114 | }
115 |
116 | // Set the active branch
117 | meta.ActiveBranch = branchActiveSetBranch
118 |
119 | // Save the updated metadata
120 | err = saveMetadata(db, meta)
121 | if err != nil {
122 | return err
123 | }
124 |
125 | _, err = fmt.Fprintf(fOut, "Branch '%s' set as active for '%s'\n", branchActiveSetBranch, db)
126 | return err
127 | }
128 |
--------------------------------------------------------------------------------
/cmd/branchCreate.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var branchCreateBranch, branchCreateCommit, branchCreateMsg string
11 |
12 | // Creates a branch for a database
13 | var branchCreateCmd = &cobra.Command{
14 | Use: "create [database name] --branch xxx --commit yyy",
15 | Short: "Create a branch for a database",
16 | RunE: func(cmd *cobra.Command, args []string) error {
17 | return branchCreate(args)
18 | },
19 | }
20 |
21 | func init() {
22 | branchCmd.AddCommand(branchCreateCmd)
23 | branchCreateCmd.Flags().StringVar(&branchCreateBranch, "branch", "", "Name of remote branch to create")
24 | branchCreateCmd.Flags().StringVar(&branchCreateCommit, "commit", "", "Commit ID for the new branch head")
25 | branchCreateCmd.Flags().StringVar(&branchCreateMsg, "description", "", "Description of the branch")
26 | }
27 |
28 | func branchCreate(args []string) error {
29 | // Ensure a database file was given
30 | var db string
31 | var err error
32 | var meta metaData
33 | if len(args) == 0 {
34 | db, err = getDefaultDatabase()
35 | if err != nil {
36 | return err
37 | }
38 | if db == "" {
39 | // No database name was given on the command line, and we don't have a default database selected
40 | return errors.New("No database file specified")
41 | }
42 | } else {
43 | db = args[0]
44 | }
45 | if len(args) > 1 {
46 | return errors.New("Only one database can be changed at a time (for now)")
47 | }
48 |
49 | // Ensure a new branch name and commit ID were given
50 | if branchCreateBranch == "" {
51 | return errors.New("No branch name given")
52 | }
53 | if branchCreateCommit == "" {
54 | return errors.New("No commit ID given")
55 | }
56 |
57 | // Load the metadata
58 | meta, err = loadMetadata(db)
59 | if err != nil {
60 | return err
61 | }
62 |
63 | // Ensure a branch with the same name doesn't already exist
64 | if _, ok := meta.Branches[branchCreateBranch]; ok == true {
65 | return errors.New("A branch with that name already exists")
66 | }
67 |
68 | // Make sure the target commit exists in our commit list
69 | c, ok := meta.Commits[branchCreateCommit]
70 | if ok != true {
71 | return errors.New("That commit isn't in the database commit list")
72 | }
73 |
74 | // Count the number of commits in the new branch
75 | numCommits := 1
76 | for c.Parent != "" {
77 | numCommits++
78 | c = meta.Commits[c.Parent]
79 | }
80 |
81 | // Generate the new branch info locally
82 | newBranch := branchEntry{
83 | Commit: branchCreateCommit,
84 | CommitCount: numCommits,
85 | Description: branchCreateMsg,
86 | }
87 |
88 | // Add the new branch to the local metadata cache
89 | meta.Branches[branchCreateBranch] = newBranch
90 |
91 | // Save the updated metadata back to disk
92 | err = saveMetadata(db, meta)
93 | if err != nil {
94 | return err
95 | }
96 |
97 | _, err = fmt.Fprintf(fOut, "Branch '%s' created\n", branchCreateBranch)
98 | return err
99 | }
100 |
--------------------------------------------------------------------------------
/cmd/branchList.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io/ioutil"
8 | "path/filepath"
9 | "sort"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | // Displays the list of branches for a remote database
15 | var branchListCmd = &cobra.Command{
16 | Use: "list [database name]",
17 | Short: "List the branches for your database on a DBHub.io cloud",
18 | RunE: func(cmd *cobra.Command, args []string) error {
19 | return branchList(args)
20 | },
21 | }
22 |
23 | func init() {
24 | branchCmd.AddCommand(branchListCmd)
25 | }
26 |
27 | func branchList(args []string) error {
28 | // Ensure a database file was given
29 | var db string
30 | var err error
31 | var meta metaData
32 | if len(args) == 0 {
33 | db, err = getDefaultDatabase()
34 | if err != nil {
35 | return err
36 | }
37 | if db == "" {
38 | // No database name was given on the command line, and we don't have a default database selected
39 | return errors.New("No database file specified")
40 | }
41 | } else {
42 | db = args[0]
43 | }
44 | if len(args) > 1 {
45 | return errors.New("Only one database can be worked with at a time (for now)")
46 | }
47 |
48 | // If there is a local metadata cache for the requested database, use that. Otherwise, retrieve it from the
49 | // server first (without storing it)
50 | meta = metaData{}
51 | md, err := ioutil.ReadFile(filepath.Join(".dio", db, "metadata.json"))
52 | if err == nil {
53 | err = json.Unmarshal([]byte(md), &meta)
54 | if err != nil {
55 | return err
56 | }
57 | } else {
58 | // No local cache, so retrieve the info from the server
59 | meta, _, err = retrieveMetadata(db)
60 | if err != nil {
61 | return err
62 | }
63 | }
64 |
65 | // Sort the list alphabetically
66 | var sortedKeys []string
67 | for k := range meta.Branches {
68 | sortedKeys = append(sortedKeys, k)
69 | }
70 | sort.Strings(sortedKeys)
71 |
72 | // Display the list of branches
73 | _, err = fmt.Fprintf(fOut, "Branches for %s:\n\n", db)
74 | if err != nil {
75 | return err
76 | }
77 | for _, i := range sortedKeys {
78 | _, err = fmt.Fprintf(fOut, " * '%s' - Commit: %s\n", i, meta.Branches[i].Commit)
79 | if err != nil {
80 | return err
81 | }
82 | if meta.Branches[i].Description != "" {
83 | _, err = fmt.Fprintf(fOut, "\n %s\n\n", meta.Branches[i].Description)
84 | if err != nil {
85 | return err
86 | }
87 | }
88 | }
89 |
90 | // Extra newline is needed in some cases for consistency
91 | finalSortedKey := sortedKeys[len(sortedKeys)-1]
92 | if meta.Branches[finalSortedKey].Description == "" {
93 | _, err = fmt.Fprintln(fOut)
94 | if err != nil {
95 | return err
96 | }
97 | }
98 | _, err = fmt.Fprintf(fOut, " Active branch: %s\n\n", meta.ActiveBranch)
99 | return err
100 | }
101 |
--------------------------------------------------------------------------------
/cmd/branchRemove.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var branchRemoveBranch string
11 |
12 | // Removes a branch from a database
13 | var branchRemoveCmd = &cobra.Command{
14 | Use: "remove [database name] --branch xxx",
15 | Short: "Removes a branch from a database",
16 | RunE: func(cmd *cobra.Command, args []string) error {
17 | return branchRemove(args)
18 | },
19 | }
20 |
21 | func init() {
22 | branchCmd.AddCommand(branchRemoveCmd)
23 | branchRemoveCmd.Flags().StringVar(&branchRemoveBranch, "branch", "", "Name of remote branch to remove")
24 | }
25 |
26 | func branchRemove(args []string) error {
27 | // Ensure a database file was given
28 | var db string
29 | var err error
30 | var meta metaData
31 | if len(args) == 0 {
32 | db, err = getDefaultDatabase()
33 | if err != nil {
34 | return err
35 | }
36 | if db == "" {
37 | // No database name was given on the command line, and we don't have a default database selected
38 | return errors.New("No database file specified")
39 | }
40 | } else {
41 | db = args[0]
42 | }
43 | if len(args) > 1 {
44 | return errors.New("Only one database can be changed at a time (for now)")
45 | }
46 |
47 | // Ensure a branch name was given
48 | if branchRemoveBranch == "" {
49 | return errors.New("No branch name given")
50 | }
51 |
52 | // Load the metadata
53 | meta, err = loadMetadata(db)
54 | if err != nil {
55 | return err
56 | }
57 |
58 | // Check if the branch exists
59 | if _, ok := meta.Branches[branchRemoveBranch]; ok != true {
60 | return errors.New("A branch with that name doesn't exist")
61 | }
62 |
63 | // If the branch is the currently active one, then abort
64 | if branchRemoveBranch == meta.ActiveBranch {
65 | return errors.New("Can't remove the currently active branch. You need to switch branches first")
66 | }
67 |
68 | // Remove the branch
69 | delete(meta.Branches, branchRemoveBranch)
70 |
71 | // Save the updated metadata back to disk
72 | err = saveMetadata(db, meta)
73 | if err != nil {
74 | return err
75 | }
76 |
77 | _, err = fmt.Fprintf(fOut, "Branch '%s' removed\n", branchRemoveBranch)
78 | return err
79 | }
80 |
--------------------------------------------------------------------------------
/cmd/branchRevert.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "time"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var (
15 | branchRevertBranch, branchRevertCommit, branchRevertTag string
16 | branchRevertForce *bool
17 | )
18 |
19 | // Reverts a database to a prior commit in its history
20 | var branchRevertCmd = &cobra.Command{
21 | Use: "revert [database name] --branch xxx --commit yyy",
22 | Short: "Resets a database branch back to a previous commit",
23 | RunE: func(cmd *cobra.Command, args []string) error {
24 | return branchRevert(args)
25 | },
26 | }
27 |
28 | func init() {
29 | branchCmd.AddCommand(branchRevertCmd)
30 | branchRevertCmd.Flags().StringVar(&branchRevertBranch, "branch", "",
31 | "Branch to operate on")
32 | branchRevertCmd.Flags().StringVar(&branchRevertCommit, "commit", "",
33 | "Commit ID for the to revert to")
34 | branchRevertForce = branchRevertCmd.Flags().BoolP("force", "f", false,
35 | "Overwrite unsaved changes to the database?")
36 | branchRevertCmd.Flags().StringVar(&branchRevertTag, "tag", "", "Name of tag to revert to")
37 | }
38 |
39 | func branchRevert(args []string) error {
40 | // Ensure a database file was given
41 | var db string
42 | var err error
43 | var meta metaData
44 | if len(args) == 0 {
45 | db, err = getDefaultDatabase()
46 | if err != nil {
47 | return err
48 | }
49 | if db == "" {
50 | // No database name was given on the command line, and we don't have a default database selected
51 | return errors.New("No database file specified")
52 | }
53 | } else {
54 | db = args[0]
55 | }
56 | if len(args) > 1 {
57 | return errors.New("Only one database can be changed at a time (for now)")
58 | }
59 |
60 | // Ensure the required info was given
61 | if branchRevertCommit == "" && branchRevertTag == "" {
62 | return errors.New("Either a commit ID or tag must be given.")
63 | }
64 |
65 | // Ensure we were given only a commit ID OR a tag
66 | if branchRevertCommit != "" && branchRevertTag != "" {
67 | return errors.New("Either a commit ID or tag must be given. Not both!")
68 | }
69 |
70 | // Load the metadata
71 | meta, err = loadMetadata(db)
72 | if err != nil {
73 | return err
74 | }
75 |
76 | // Unless --force is specified, check whether the file has changed since the last commit, and let the user know
77 | if *branchRevertForce == false {
78 | changed, err := dbChanged(db, meta)
79 | if err != nil {
80 | return err
81 | }
82 | if changed {
83 | _, err = fmt.Fprintf(fOut, "%s has been changed since the last commit. Use --force if you "+
84 | "really want to overwrite it\n", db)
85 | return err
86 | }
87 | }
88 |
89 | // If a tag was given, make sure it exists
90 | if branchRevertTag != "" {
91 | tagData, ok := meta.Tags[branchRevertTag]
92 | if !ok {
93 | return errors.New("That tag doesn't exist")
94 | }
95 |
96 | // Use the commit associated with the tag
97 | branchRevertCommit = tagData.Commit
98 | }
99 |
100 | // If no branch name was passed, use the active branch
101 | if branchRevertBranch == "" {
102 | branchRevertBranch = meta.ActiveBranch
103 | }
104 |
105 | // Make sure the branch exists
106 | matchFound := false
107 | head, ok := meta.Branches[branchRevertBranch]
108 | if ok == false {
109 | return errors.New("That branch doesn't exist")
110 | }
111 | if head.Commit == branchRevertCommit {
112 | matchFound = true
113 | }
114 | delList := map[string]struct{}{}
115 | if !matchFound {
116 | delList[head.Commit] = struct{}{} // Start creating a list of the branch commits to be deleted
117 | }
118 |
119 | // Build a list of commits in the branch
120 | commitList := []string{head.Commit}
121 | c, ok := meta.Commits[head.Commit]
122 | if ok == false {
123 | return errors.New("Something has gone wrong. Head commit for the branch isn't in the commit list")
124 | }
125 | for c.Parent != "" {
126 | c = meta.Commits[c.Parent]
127 | if c.ID == branchRevertCommit {
128 | matchFound = true
129 | }
130 | if !matchFound {
131 | delList[c.ID] = struct{}{} // Only commits prior to matchFound should be deleted
132 | }
133 | commitList = append(commitList, c.ID)
134 | }
135 |
136 | // Make sure the requested commit exists on the selected branch
137 | if !matchFound {
138 | return errors.New("The given commit or tag doesn't seem to exist on the selected branch")
139 | }
140 |
141 | // Make sure the correct database from the target branch is in local cache
142 | var shaSum string
143 | var lastMod time.Time
144 | if branchRevertCommit != "" {
145 | shaSum = meta.Commits[branchRevertCommit].Tree.Entries[0].Sha256
146 | lastMod = meta.Commits[branchRevertCommit].Tree.Entries[0].LastModified
147 |
148 | // Fetch the database from DBHub.io if it's not in the local cache
149 | err = checkDBCache(db, shaSum)
150 | if err != nil {
151 | return err
152 | }
153 | } else {
154 | return errors.New("Haven't been able to determine branch name. This shouldn't happen")
155 | }
156 |
157 | // Check if deleting the commits would leave isolated tags or releases. If so, abort and warn the user
158 | type isolCheck struct {
159 | safe bool
160 | commit string
161 | }
162 | var isolatedTags []string
163 | var isolatedReleases []string
164 | commitTags := map[string]isolCheck{}
165 | commitReleases := map[string]isolCheck{}
166 | for delCommit := range delList {
167 | // Ensure that deleting this commit won't result in any isolated/unreachable tags
168 | for tName, tEntry := range meta.Tags {
169 | // Scan through the database tag list, checking if any of the tags is for the commit we're deleting
170 | if tEntry.Commit == delCommit {
171 | commitTags[tName] = isolCheck{safe: false, commit: delCommit}
172 | }
173 | }
174 |
175 | // Ensure that deleting this commit won't result in any isolated/unreachable releases
176 | for rName, rEntry := range meta.Releases {
177 | // Scan through the database release list, checking if any of the releases is for the commit we're
178 | // deleting
179 | if rEntry.Commit == delCommit {
180 | commitReleases[rName] = isolCheck{safe: false, commit: delCommit}
181 | }
182 | }
183 | }
184 |
185 | if len(commitTags) > 0 {
186 | // If a commit we're deleting has a tag on it, we need to check whether the commit is on other branches too
187 | // * If it is, we're ok to proceed as the tag can still be reached from the other branch(es)
188 | // * If it isn't, we need to abort this deletion (and tell the user), as the tag would become unreachable
189 | for bName, bEntry := range meta.Branches {
190 | if bName == branchRevertBranch {
191 | // We only run this comparison from "other branches", not the branch whose history we're changing
192 | continue
193 | }
194 | c, ok = meta.Commits[bEntry.Commit]
195 | if !ok {
196 | return fmt.Errorf("Broken commit history encountered when checking for isolated tags "+
197 | "while reverting in branch '%s' of database '%s'\n", branchRevertBranch, db)
198 | }
199 | for tName, tEntry := range commitTags {
200 | if c.ID == tEntry.commit {
201 | // The commit is also on another branch, so we're ok to delete the commit
202 | tmp := commitTags[tName]
203 | tmp.safe = true
204 | commitTags[tName] = tmp
205 | }
206 | }
207 | for c.Parent != "" {
208 | c, ok = meta.Commits[c.Parent]
209 | if !ok {
210 | return fmt.Errorf("Broken commit history encountered when checking for isolated tags "+
211 | "while reverting in branch '%s' of database '%s'\n", branchRevertBranch, db)
212 | }
213 | for tName, tEntry := range commitTags {
214 | if c.ID == tEntry.commit {
215 | // The commit is also on another branch, so we're ok to delete the commit
216 | tmp := commitTags[tName]
217 | tmp.safe = true
218 | commitTags[tName] = tmp
219 | }
220 | }
221 | }
222 | }
223 |
224 | // Create a list of would-be-isolated tags
225 | for tName, tEntry := range commitTags {
226 | if tEntry.safe == false {
227 | isolatedTags = append(isolatedTags, tName)
228 | }
229 | }
230 | }
231 |
232 | if len(commitReleases) > 0 {
233 | // If a commit we're deleting has a release on it, we need to check whether the commit is on other branches too
234 | // * If it is, we're ok to proceed as the release can still be reached from the other branch(es)
235 | // * If it isn't, we need to abort this deletion (and tell the user), as the release would become unreachable
236 | for bName, bEntry := range meta.Branches {
237 | if bName == branchRevertBranch {
238 | // We only run this comparison from "other branches", not the branch whose history we're changing
239 | continue
240 | }
241 | c, ok = meta.Commits[bEntry.Commit]
242 | if !ok {
243 | return fmt.Errorf("Broken commit history encountered when checking for isolated releases "+
244 | "while reverting in branch '%s' of database '%s'\n", branchRevertBranch, db)
245 | }
246 | for rName, rEntry := range commitReleases {
247 | if c.ID == rEntry.commit {
248 | // The commit is also on another branch, so we're ok to delete the commit
249 | tmp := commitReleases[rName]
250 | tmp.safe = true
251 | commitReleases[rName] = tmp
252 | }
253 | }
254 | for c.Parent != "" {
255 | c, ok = meta.Commits[c.Parent]
256 | if !ok {
257 | return fmt.Errorf("Broken commit history encountered when checking for isolated "+
258 | "releases while reverting in branch '%s' of database '%s'\n", branchRevertBranch, db)
259 | }
260 | for rName, rEntry := range commitReleases {
261 | if c.ID == rEntry.commit {
262 | // The commit is also on another branch, so we're ok to delete the commit
263 | tmp := commitReleases[rName]
264 | tmp.safe = true
265 | commitReleases[rName] = tmp
266 | }
267 | }
268 | }
269 | }
270 |
271 | // Create a list of would-be-isolated releases
272 | for rName, rEntry := range commitReleases {
273 | if rEntry.safe == false {
274 | isolatedReleases = append(isolatedReleases, rName)
275 | }
276 | }
277 | }
278 |
279 | // If any tags or releases would be isolated, abort
280 | if len(isolatedTags) > 0 || len(isolatedReleases) > 0 {
281 | e := fmt.Sprint("You need to remove the following tags and releases before reverting to this " +
282 | "commit:\n\n")
283 | for _, j := range isolatedTags {
284 | e = fmt.Sprintf("%s * tag '%s'\n", e, j)
285 | }
286 | for _, j := range isolatedReleases {
287 | e = fmt.Sprintf("%s * release '%s'\n", e, j)
288 | }
289 | return errors.New(e)
290 | }
291 |
292 | // Count the number of commits in the updated branch
293 | var commitCount int
294 | listLen := len(commitList) - 1
295 | for i := 0; i <= listLen; i++ {
296 | commitCount++
297 | if commitList[listLen-i] == branchRevertCommit {
298 | break
299 | }
300 | }
301 |
302 | // Revert the branch
303 | // TODO: Remove the no-longer-referenced commits (if any) caused by this revert
304 | // * One alternative would be to leave them, and only clean up with with some kind of garbage collection
305 | // operation. Even a "dio gc" to manually trigger it
306 | newHead := branchEntry{
307 | Commit: branchRevertCommit,
308 | CommitCount: commitCount,
309 | Description: head.Description,
310 | }
311 | meta.Branches[branchRevertBranch] = newHead
312 |
313 | // Copy the file from local cache to the working directory
314 | var b []byte
315 | b, err = ioutil.ReadFile(filepath.Join(".dio", db, "db", shaSum))
316 | if err != nil {
317 | return err
318 | }
319 | err = ioutil.WriteFile(db, b, 0644)
320 | if err != nil {
321 | return err
322 | }
323 | err = os.Chtimes(db, time.Now(), lastMod)
324 | if err != nil {
325 | return err
326 | }
327 |
328 | // Save the updated metadata back to disk
329 | err = saveMetadata(db, meta)
330 | if err != nil {
331 | return err
332 | }
333 |
334 | _, err = fmt.Fprintln(fOut, "Branch reverted")
335 | return err
336 | }
337 |
--------------------------------------------------------------------------------
/cmd/branchUpdate.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var branchUpdateBranch, branchUpdateMsg string
11 | var descDel *bool
12 |
13 | // Updates the description text for a branch
14 | var branchUpdateCmd = &cobra.Command{
15 | Use: "update [database name]",
16 | Short: "Update the description for a branch",
17 | RunE: func(cmd *cobra.Command, args []string) error {
18 | return branchUpdate(args)
19 | },
20 | }
21 |
22 | func init() {
23 | branchCmd.AddCommand(branchUpdateCmd)
24 | branchUpdateCmd.Flags().StringVar(&branchUpdateBranch, "branch", "",
25 | "Name of branch to update")
26 | descDel = branchUpdateCmd.Flags().BoolP("delete", "d", false,
27 | "Delete the branch description")
28 | branchUpdateCmd.Flags().StringVar(&branchUpdateMsg, "description", "",
29 | "New description for the branch")
30 | }
31 |
32 | func branchUpdate(args []string) error {
33 | // Ensure a database file was given
34 | var db string
35 | var err error
36 | var meta metaData
37 | if len(args) == 0 {
38 | db, err = getDefaultDatabase()
39 | if err != nil {
40 | return err
41 | }
42 | if db == "" {
43 | // No database name was given on the command line, and we don't have a default database selected
44 | return errors.New("No database file specified")
45 | }
46 | } else {
47 | db = args[0]
48 | }
49 | if len(args) > 1 {
50 | return errors.New("Only one database can be changed at a time (for now)")
51 | }
52 |
53 | // Ensure a branch name and description text were given
54 | if branchUpdateBranch == "" {
55 | return errors.New("No branch name given")
56 | }
57 | if branchUpdateMsg == "" && *descDel == false {
58 | return errors.New("No description text given")
59 | }
60 |
61 | // Load the metadata
62 | meta, err = loadMetadata(db)
63 | if err != nil {
64 | return err
65 | }
66 |
67 | // Make sure the branch exists
68 | branch, ok := meta.Branches[branchUpdateBranch]
69 | if ok == false {
70 | return errors.New("That branch doesn't exist")
71 | }
72 |
73 | // Update the branch
74 | if *descDel == false {
75 | branch.Description = branchUpdateMsg
76 | } else {
77 | branch.Description = ""
78 | }
79 | meta.Branches[branchUpdateBranch] = branch
80 |
81 | // Save the updated metadata back to disk
82 | err = saveMetadata(db, meta)
83 | if err != nil {
84 | return err
85 | }
86 |
87 | // Inform the user
88 | _, err = fmt.Fprintln(fOut, "Branch updated")
89 | return err
90 | }
91 |
--------------------------------------------------------------------------------
/cmd/commit.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "crypto/sha256"
5 | "encoding/hex"
6 | "fmt"
7 | "io/ioutil"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 | "time"
12 |
13 | "github.com/pkg/errors"
14 | "github.com/spf13/cobra"
15 | "github.com/spf13/viper"
16 | )
17 |
18 | var (
19 | commitCmdAuthEmail, commitCmdAuthName, commitCmdBranch, commitCmdCommit string
20 | commitCmdLicence, commitCmdMsg, commitCmdTimestamp string
21 | )
22 |
23 | // Create a commit for the database on the currently active branch
24 | var (
25 | commitCmd = &cobra.Command{
26 | Use: "commit [database file]",
27 | Short: "Creates a new commit for the database",
28 | RunE: func(cmd *cobra.Command, args []string) error {
29 | return commit(args)
30 | },
31 | }
32 | )
33 |
34 | func init() {
35 | RootCmd.AddCommand(commitCmd)
36 | commitCmd.Flags().StringVar(&commitCmdBranch, "branch", "",
37 | "The branch this commit will be appended to")
38 | commitCmd.Flags().StringVar(&commitCmdCommit, "commit", "",
39 | "ID of the previous commit, for appending this new database to")
40 | commitCmd.Flags().StringVar(&commitCmdAuthEmail, "email", "",
41 | "Email address of the commit author")
42 | commitCmd.Flags().StringVar(&commitCmdLicence, "licence", "",
43 | "The licence (ID) for the database, as per 'dio licence list'")
44 | commitCmd.Flags().StringVar(&commitCmdMsg, "message", "",
45 | "Description / commit message")
46 | commitCmd.Flags().StringVar(&commitCmdAuthName, "name", "", "Name of the commit author")
47 | commitCmd.Flags().StringVar(&commitCmdTimestamp, "timestamp", "", "Timestamp for the commit")
48 | }
49 |
50 | func commit(args []string) error {
51 | // Ensure a database file was given
52 | var db string
53 | var err error
54 | var meta metaData
55 | if len(args) == 0 {
56 | db, err = getDefaultDatabase()
57 | if err != nil {
58 | return err
59 | }
60 | if db == "" {
61 | // No database name was given on the command line, and we don't have a default database selected
62 | return errors.New("No database file specified")
63 | }
64 | } else {
65 | db = args[0]
66 | }
67 | // TODO: Allow giving multiple database files on the command line. Hopefully just needs turning this
68 | // TODO into a for loop
69 | if len(args) > 1 {
70 | return errors.New("Only one database can be uploaded at a time (for now)")
71 | }
72 |
73 | // Ensure the database file exists
74 | fi, err := os.Stat(db)
75 | if err != nil {
76 | return err
77 | }
78 |
79 | // Grab author name & email from the dio config file, but allow command line flags to override them
80 | var authorName, authorEmail, committerName, committerEmail string
81 | if z, ok := viper.Get("user.name").(string); ok {
82 | authorName = z
83 | committerName = z
84 | }
85 | if z, ok := viper.Get("user.email").(string); ok {
86 | authorEmail = z
87 | committerEmail = z
88 | }
89 | if commitCmdAuthName != "" {
90 | authorName = commitCmdAuthName
91 | }
92 | if commitCmdAuthEmail != "" {
93 | authorEmail = commitCmdAuthEmail
94 | }
95 |
96 | // Author name and email are required
97 | if authorName == "" || authorEmail == "" || committerName == "" || committerEmail == "" {
98 | return errors.New("Author and committer name and email addresses are required!")
99 | }
100 |
101 | // If a timestamp was provided, make sure it parses ok
102 | commitTime := time.Now()
103 | if commitCmdTimestamp != "" {
104 | commitTime, err = time.Parse(time.RFC3339, commitCmdTimestamp)
105 | if err != nil {
106 | return err
107 | }
108 | }
109 |
110 | // If the database metadata doesn't exist locally, check if it does exist on the server.
111 | var newDB, localPresent bool
112 | if _, err = os.Stat(filepath.Join(".dio", db, "db")); os.IsNotExist(err) {
113 | // At the moment, since there's no better way to check for the existence of a remote database, we just
114 | // grab the list of the users databases and check against that
115 | dbList, errInner := getDatabases(cloud, certUser)
116 | if errInner != nil {
117 | return errInner
118 | }
119 | for _, j := range dbList {
120 | if db == j.Name {
121 | // This database already exists on DBHub.io. We need local metadata in order to proceed, but don't
122 | // yet have it. Safest option, at least for now, is to tell the user and abort
123 | return errors.New("Aborting: the database exists on the remote server, but has no " +
124 | "local metadata cache. Please retrieve the remote metadata, then run the commit command again")
125 | }
126 | }
127 |
128 | // This is a new database, so we generate new metadata
129 | newDB = true
130 | meta = newMetaStruct(commitCmdBranch)
131 | } else {
132 | // We have local metaData
133 | localPresent = true
134 | }
135 |
136 | // Load the metadata
137 | if !newDB {
138 | meta, err = loadMetadata(db)
139 | if err != nil {
140 | return err
141 | }
142 | }
143 |
144 | // If no branch name was passed, use the active branch
145 | if commitCmdBranch == "" {
146 | commitCmdBranch = meta.ActiveBranch
147 | }
148 |
149 | // Check if the database is unchanged from the previous commit, and if so we abort the commit
150 | if localPresent {
151 | changed, err := dbChanged(db, meta)
152 | if err != nil {
153 | return err
154 | }
155 | if !changed && commitCmdLicence == "" {
156 | return fmt.Errorf("Database is unchanged from last commit. No need to commit anything.")
157 | }
158 | }
159 |
160 | // Get the current head commit for the selected branch, as that will be the parent commit for this new one
161 | head, ok := meta.Branches[commitCmdBranch]
162 | if !ok {
163 | return errors.New(fmt.Sprintf("That branch ('%s') doesn't exist", commitCmdBranch))
164 | }
165 | var existingLicSHA string
166 | if newDB {
167 | if commitCmdLicence == "" {
168 | // If this is a new database, and no licence was given on the command line, then default to
169 | // 'Not specified'
170 | commitCmdLicence = "Not specified"
171 | }
172 | } else {
173 | if localPresent {
174 | // We can only use commit data if local metadata is present
175 | headCommit, ok := meta.Commits[head.Commit]
176 | if !ok {
177 | return errors.New("Aborting: info for the head commit isn't found in the local commit cache")
178 | }
179 | existingLicSHA = headCommit.Tree.Entries[0].LicenceSHA
180 | }
181 | }
182 |
183 | // Retrieve the list of known licences
184 | licList, err := getLicences()
185 | if err != nil {
186 | return err
187 | }
188 |
189 | // Determine the SHA256 of the requested licence
190 | var licID, licSHA string
191 | if commitCmdLicence != "" {
192 | // Scan the licence list for a matching licence name
193 | matchFound := false
194 | lwrLic := strings.ToLower(commitCmdLicence)
195 | for i, j := range licList {
196 | if strings.ToLower(i) == lwrLic {
197 | licID = i
198 | licSHA = j.Sha256
199 | matchFound = true
200 | break
201 | }
202 | }
203 | if !matchFound {
204 | return errors.New("Aborting: could not determine the name of the existing database licence")
205 | }
206 | } else {
207 | // If no licence was given, use the licence from the previous commit
208 | licSHA = existingLicSHA
209 | }
210 |
211 | // Generate an appropriate commit message if none was provided
212 | if commitCmdMsg == "" {
213 | if !newDB && existingLicSHA != licSHA {
214 | // * The licence has changed, so we create a reasonable commit message indicating this *
215 |
216 | // Work out the human friendly short licence name for the current database
217 | matchFound := false
218 | var existingLicID string
219 | for i, j := range licList {
220 | if existingLicSHA == j.Sha256 {
221 | existingLicID = i
222 | matchFound = true
223 | break
224 | }
225 | }
226 | if !matchFound {
227 | return errors.New("Aborting: could not locate the requested database licence")
228 | }
229 | commitCmdMsg = fmt.Sprintf("Database licence changed from '%s' to '%s'.", existingLicID, licID)
230 | }
231 |
232 | // If it's a new database and there's still no commit message, generate a reasonable one
233 | if newDB && commitCmdMsg == "" {
234 | commitCmdMsg = "New database created"
235 | }
236 | }
237 |
238 | // * Collect info for the new commit *
239 |
240 | // Get file size and last modified time for the database
241 | fileSize := fi.Size()
242 | lastModified := fi.ModTime()
243 |
244 | // Verify we've read the file from disk ok
245 | b, err := ioutil.ReadFile(db)
246 | if err != nil {
247 | return err
248 | }
249 | if int64(len(b)) != fileSize {
250 | return errors.New(numFormat.Sprintf("Aborting: # of bytes read (%d) when generating commit don't "+
251 | "match database file size (%d)", len(b), fileSize))
252 | }
253 |
254 | // Generate sha256
255 | s := sha256.Sum256(b)
256 | shaSum := hex.EncodeToString(s[:])
257 |
258 | // * Generate the new commit *
259 |
260 | // Create a new dbTree entry for the database file
261 | var e dbTreeEntry
262 | e.EntryType = DATABASE
263 | e.LastModified = lastModified.UTC()
264 | e.LicenceSHA = licSHA
265 | e.Name = db
266 | e.Sha256 = shaSum
267 | e.Size = fileSize
268 |
269 | // Create a new dbTree structure for the new database entry
270 | var t dbTree
271 | t.Entries = append(t.Entries, e)
272 | t.ID = createDBTreeID(t.Entries)
273 |
274 | // Create a new commit for the new tree
275 | newCom := commitEntry{
276 | AuthorName: authorName,
277 | AuthorEmail: authorEmail,
278 | CommitterName: committerName,
279 | CommitterEmail: committerEmail,
280 | Message: commitCmdMsg,
281 | Parent: head.Commit,
282 | Timestamp: commitTime.UTC(),
283 | Tree: t,
284 | }
285 |
286 | // Calculate the new commit ID, which incorporates the updated tree ID (and thus the new licence sha256)
287 | newCom.ID = createCommitID(newCom)
288 |
289 | // Add the new commit info to the database commit list
290 | meta.Commits[newCom.ID] = newCom
291 |
292 | // Update the branch head info to point at the new commit
293 | meta.Branches[commitCmdBranch] = branchEntry{
294 | Commit: newCom.ID,
295 | CommitCount: head.CommitCount + 1,
296 | Description: head.Description,
297 | }
298 |
299 | // If the database file isn't already in the local cache, then copy it there
300 | if _, err = os.Stat(filepath.Join(".dio", db, "db", shaSum)); os.IsNotExist(err) {
301 | if _, err = os.Stat(filepath.Join(".dio", db)); os.IsNotExist(err) {
302 | err = os.MkdirAll(filepath.Join(".dio", db, "db"), 0770)
303 | if err != nil {
304 | return err
305 | }
306 | }
307 | err = ioutil.WriteFile(filepath.Join(".dio", db, "db", shaSum), b, 0644)
308 | if err != nil {
309 | return err
310 | }
311 | }
312 |
313 | // Save the updated metadata back to disk
314 | err = saveMetadata(db, meta)
315 | if err != nil {
316 | return err
317 | }
318 |
319 | // Display results to the user
320 | _, err = fmt.Fprintf(fOut, "Commit created on '%s'\n", db)
321 | if err != nil {
322 | return err
323 | }
324 | _, err = fmt.Fprintf(fOut, " * Commit ID: %s\n", newCom.ID)
325 | if err != nil {
326 | return err
327 | }
328 | _, err = fmt.Fprintf(fOut, " Branch: %s\n", commitCmdBranch)
329 | if err != nil {
330 | return err
331 | }
332 | if licID != "" {
333 | _, err = fmt.Fprintf(fOut, " Licence: %s\n", licID)
334 | if err != nil {
335 | return err
336 | }
337 | }
338 | _, err = numFormat.Fprintf(fOut, " Size: %d bytes\n", e.Size)
339 | if err != nil {
340 | return err
341 | }
342 | if commitCmdMsg != "" {
343 | _, err = fmt.Fprintf(fOut, " Commit message: %s\n\n", commitCmdMsg)
344 | if err != nil {
345 | return err
346 | }
347 | }
348 | return nil
349 | }
350 |
351 | // Creates a new metadata structure in memory
352 | func newMetaStruct(branch string) (meta metaData) {
353 | b := branchEntry{
354 | Commit: "",
355 | CommitCount: 0,
356 | Description: "",
357 | }
358 | var initialBranch string
359 | if branch == "" {
360 | initialBranch = "main"
361 | } else {
362 | initialBranch = branch
363 | }
364 | meta = metaData{
365 | ActiveBranch: initialBranch,
366 | Branches: map[string]branchEntry{initialBranch: b},
367 | Commits: map[string]commitEntry{},
368 | DefBranch: initialBranch,
369 | Releases: map[string]releaseEntry{},
370 | Tags: map[string]tagEntry{},
371 | }
372 | return
373 | }
374 |
--------------------------------------------------------------------------------
/cmd/info.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/spf13/cobra"
7 | "github.com/spf13/viper"
8 | )
9 |
10 | // Displays useful information about the dio installation
11 | var infoCmd = &cobra.Command{
12 | Use: "info",
13 | Short: "Displays useful information about the dio installation",
14 | RunE: func(cmd *cobra.Command, args []string) error {
15 | fmt.Printf("Dio version %s\n", DIO_VERSION)
16 |
17 | // Display the path to the dio configuration file
18 | if confPath := viper.ConfigFileUsed(); confPath != "" {
19 | fmt.Println("Configuration file used:", confPath)
20 | }
21 |
22 | fmt.Printf("\n** Connection **\n\n")
23 |
24 | // Display the connection URL used for DBHUB.io
25 | if found := viper.IsSet("general.cloud"); found == true {
26 | fmt.Printf("DBHub.io connection URL: %s\n", viper.Get("general.cloud"))
27 | } else {
28 | fmt.Println("No custom DBHub.io connection URL is set")
29 | }
30 |
31 | // Display the path to our CA Chain and user certificate
32 | if found := viper.IsSet("certs.cachain"); found == true {
33 | fmt.Printf("Path to CA chain file: %s\n", viper.Get("certs.cachain"))
34 | } else {
35 | fmt.Println("Path to CA chain not set in configuration file")
36 | }
37 | if found := viper.IsSet("certs.cert"); found == true {
38 | fmt.Printf("Path to user certificate file: %s\n", viper.Get("certs.cert"))
39 | } else {
40 | fmt.Println("Path to user certificate not set in configuration file")
41 | }
42 |
43 | // TODO: Maybe display the user name, server, and expiry date from the cert file?
44 |
45 | fmt.Printf("\n** Commit defaults **\n\n")
46 |
47 | // Display the user name and email address used for commits
48 | if found := viper.IsSet("user.name"); found == true {
49 | fmt.Printf("User name for commits: %s\n", viper.Get("user.name"))
50 | } else {
51 | fmt.Println("User name not set in configuration file")
52 | }
53 | if found := viper.IsSet("user.email"); found == true {
54 | fmt.Printf("Email address for commits: %s\n", viper.Get("user.email"))
55 | } else {
56 | fmt.Println("Email address not set in configuration file")
57 | }
58 | return nil
59 | },
60 | }
61 |
62 | func init() {
63 | RootCmd.AddCommand(infoCmd)
64 | }
65 |
--------------------------------------------------------------------------------
/cmd/licence.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | // licenceCmd represents the licence command
8 | var licenceCmd = &cobra.Command{
9 | Use: "licence",
10 | Short: "List, retrieve, update and remove licences on DBHub.io",
11 | Long: `List, retrieve, update and remove licences on DBHub.io
12 |
13 | The special word 'all' can be used with 'get' for retrieving all licences.`,
14 | Example: `
15 | $ dio licence get CC0
16 | Downloading licences...
17 |
18 | * CC0: Licence 'CC0.txt' downloaded
19 |
20 | Completed
21 |
22 | $ dio licence get all
23 | Downloading licences...
24 |
25 | * CC-BY-NC-4.0: Licence 'CC-BY-NC-4.0.txt' downloaded
26 | * CC-BY-SA-4.0: Licence 'CC-BY-SA-4.0.txt' downloaded
27 | * CC0: Licence 'CC0.txt' downloaded
28 | * ODbL-1.0: Licence 'ODbL-1.0.txt' downloaded
29 | * UK-OGL-3: Licence 'UK-OGL-3.html' downloaded
30 | * CC-BY-4.0: Licence 'CC-BY-4.0.txt' downloaded
31 | * CC-BY-IGO-3.0: Licence 'CC-BY-IGO-3.0.html' downloaded
32 |
33 | Completed`,
34 | }
35 |
36 | func init() {
37 | RootCmd.AddCommand(licenceCmd)
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/licenceAdd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 | "net/url"
8 | "os"
9 |
10 | rq "github.com/parnurzeal/gorequest"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var licenceAddFile, licenceAddFileFormat, licenceAddFullName, licenceAddURL string
15 | var licenceAddDisplayOrder int
16 |
17 | // Adds a licence to the list of known licences on the server
18 | var licenceAddCmd = &cobra.Command{
19 | Use: "add [licence name]",
20 | Short: "Add a licence to the list of known licences on a DBHub.io cloud",
21 | RunE: func(cmd *cobra.Command, args []string) error {
22 | return licenceAdd(args)
23 | },
24 | }
25 |
26 | func init() {
27 | licenceCmd.AddCommand(licenceAddCmd)
28 | licenceAddCmd.Flags().IntVar(&licenceAddDisplayOrder, "display-order", 0,
29 | "Used when displaying a list of available licences. This adjusts the position in the list.")
30 | licenceAddCmd.Flags().StringVar(&licenceAddFileFormat, "file-format", "text",
31 | "The content format of the file. Either text or html")
32 | licenceAddCmd.Flags().StringVar(&licenceAddFullName, "full-name", "",
33 | "The full name of the licence")
34 | licenceAddCmd.Flags().StringVar(&licenceAddFile, "licence-file", "",
35 | "Path to a file containing the licence as text")
36 | licenceAddCmd.Flags().StringVar(&licenceAddURL, "source-url", "",
37 | "Optional reference URL for the licence")
38 | }
39 |
40 | func licenceAdd(args []string) error {
41 | // Ensure a short licence name is present
42 | if len(args) == 0 {
43 | return errors.New("A short licence name or identifier is needed. eg CC0-BY-1.0")
44 | }
45 | if len(args) > 1 {
46 | return errors.New("Only one licence can be added at a time (for now)")
47 | }
48 |
49 | // Ensure a display order was specified
50 | if licenceAddDisplayOrder == 0 {
51 | return errors.New("A (unique) display order # must be given")
52 | }
53 |
54 | // Ensure a licence file was specified, and that it exists
55 | if licenceAddFile == "" {
56 | return errors.New("A file containing the licence text is required")
57 | }
58 | _, err := os.Stat(licenceAddFile)
59 | if err != nil {
60 | return err
61 | }
62 |
63 | // Send the licence info to the API server
64 | name := args[0]
65 | req := rq.New().TLSClientConfig(&TLSConfig).Post(fmt.Sprintf("%s/licence/add", cloud)).
66 | Type("multipart").
67 | Query(fmt.Sprintf("licence_id=%s", url.QueryEscape(name))).
68 | Query(fmt.Sprintf("display_order=%d", licenceAddDisplayOrder)).
69 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)).
70 | SendFile(licenceAddFile, "", "file1")
71 | if licenceAddFileFormat != "" {
72 | req.Query(fmt.Sprintf("file_format=%s", url.QueryEscape(licenceAddFileFormat)))
73 | }
74 | if licenceAddFullName != "" {
75 | req.Query(fmt.Sprintf("licence_name=%s", url.QueryEscape(licenceAddFullName)))
76 | }
77 | if licenceAddURL != "" {
78 | req.Query(fmt.Sprintf("source_url=%s", url.QueryEscape(licenceAddURL)))
79 | }
80 | resp, body, errs := req.End()
81 | if errs != nil {
82 | _, err = fmt.Fprint(fOut, "Errors when adding licence:")
83 | if err != nil {
84 | return err
85 | }
86 | for _, errInner := range errs {
87 | errTxt := errInner.Error()
88 | _, errInnerInner := fmt.Fprint(fOut, errTxt)
89 | if errInnerInner != nil {
90 | return errInnerInner
91 | }
92 | }
93 | return errors.New("Error when adding licence")
94 | }
95 | if resp.StatusCode != http.StatusCreated {
96 | if resp.StatusCode == http.StatusConflict {
97 | return errors.New(body)
98 | }
99 |
100 | return errors.New(fmt.Sprintf("Adding licence failed with an error: HTTP status %d - '%v'\n",
101 | resp.StatusCode, resp.Status))
102 | }
103 |
104 | _, err = fmt.Fprintf(fOut, "Licence '%s' added\n", name)
105 | return err
106 | }
107 |
--------------------------------------------------------------------------------
/cmd/licenceGet.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "net/http"
9 | "strings"
10 |
11 | rq "github.com/parnurzeal/gorequest"
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | // Downloads a licence from a DBHub.io cloud.
16 | var licenceGetCmd = &cobra.Command{
17 | Use: "get [licence name]",
18 | Short: "Downloads the text for a licence from a DBHub.io cloud, saving it to [licence name].txt",
19 | RunE: func(cmd *cobra.Command, args []string) error {
20 | return licenceGet(args)
21 | },
22 | }
23 |
24 | func init() {
25 | licenceCmd.AddCommand(licenceGetCmd)
26 | }
27 |
28 | func licenceGet(args []string) error {
29 | // Ensure a licence name was given
30 | if len(args) == 0 {
31 | return errors.New("No licence name specified")
32 | }
33 |
34 | // Check for the presence of "all" as a licence name
35 | var licenceList []string
36 | var allFound bool
37 | for _, j := range args {
38 | if strings.ToLower(j) == "all" {
39 | allFound = true
40 | }
41 | }
42 |
43 | // If the all keyword was given, then assemble the full licence list. Otherwise just use whatever was given
44 | // on the command line
45 | if allFound {
46 | l, err := getLicences()
47 | if err != nil {
48 | return errors.New(fmt.Sprintf("Error when retrieving list of all licences: %s", err))
49 | }
50 | for i := range l {
51 | licenceList = append(licenceList, i)
52 | }
53 | } else {
54 | licenceList = args
55 | }
56 |
57 | // Download the licence text
58 | dlStatus := make(map[string]string)
59 | for _, lic := range licenceList {
60 | resp, body, errs := rq.New().TLSClientConfig(&TLSConfig).Get(cloud+"/licence/get").
61 | Query(fmt.Sprintf("licence=%s", lic)).
62 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)).
63 | End()
64 | if errs != nil {
65 | for _, err := range errs {
66 | log.Print(err.Error())
67 | }
68 | dlStatus[lic] = "Error when downloading licence text"
69 | continue
70 | }
71 | if resp.StatusCode != http.StatusOK {
72 | if resp.StatusCode == http.StatusNotFound {
73 | dlStatus[lic] = "Requested licence not found"
74 | continue
75 | }
76 | dlStatus[lic] = fmt.Sprintf("Download failed with an error: HTTP status %d - '%v'",
77 | resp.StatusCode, resp.Status)
78 | continue
79 | }
80 |
81 | // Write the licence to disk
82 | var ext string
83 | if resp.Header.Get("Content-Type") == "text/html" {
84 | ext = "html"
85 | } else {
86 | ext = "txt"
87 | }
88 | err := ioutil.WriteFile(fmt.Sprintf("%s.%s", lic, ext), []byte(body), 0644)
89 | if err != nil {
90 | dlStatus[lic] = err.Error()
91 | }
92 | dlStatus[lic] = fmt.Sprintf("Licence '%s.%s' downloaded", lic, ext)
93 | }
94 |
95 | // Display the status of the individual licence downloads
96 | _, err := fmt.Fprintf(fOut, "Downloading licences from: %s...\n\n", cloud)
97 | if err != nil {
98 | return err
99 | }
100 | for i, j := range dlStatus {
101 | _, err := fmt.Fprintf(fOut, " * %s: %s\n", i, j)
102 | if err != nil {
103 | return err
104 | }
105 | }
106 | _, err = fmt.Fprintf(fOut, "\nCompleted\n")
107 | return err
108 | }
109 |
--------------------------------------------------------------------------------
/cmd/licenceList.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var licenceListDisplayOrder bool
11 |
12 | // Custom slice types, used for sorting the licences by display order
13 | type displayOrder struct {
14 | order int
15 | key string
16 | }
17 |
18 | func (p displayOrder) String() string {
19 | return fmt.Sprintf("Licence ID: %v, Display order: %v", p.key, p.order)
20 | }
21 |
22 | type displayOrderSlice []displayOrder
23 |
24 | func (p displayOrderSlice) Len() int {
25 | return len(p)
26 | }
27 |
28 | func (p displayOrderSlice) Swap(i, j int) {
29 | p[i], p[j] = p[j], p[i]
30 | }
31 |
32 | func (p displayOrderSlice) Less(i, j int) bool {
33 | return p[i].order < p[j].order
34 | }
35 |
36 | // Displays a list of the available licences.
37 | var licenceListCmd = &cobra.Command{
38 | Use: "list",
39 | Short: "Displays a list of the known licences",
40 | RunE: func(cmd *cobra.Command, args []string) error {
41 | return licenceList()
42 | },
43 | }
44 |
45 | func init() {
46 | licenceCmd.AddCommand(licenceListCmd)
47 | licenceListCmd.Flags().BoolVar(&licenceListDisplayOrder, "display-order", false,
48 | "Show the display order number of each licence")
49 | }
50 |
51 | func licenceList() error {
52 | // Retrieve the list of known licences
53 | licList, err := getLicences()
54 | if err != nil {
55 | return err
56 | }
57 |
58 | // Display the list of licences
59 | if len(licList) == 0 {
60 | _, err = fmt.Fprintf(fOut, "Cloud '%s' knows no licences\n", cloud)
61 | if err != nil {
62 | return err
63 | }
64 | return nil
65 | }
66 | _, err = fmt.Fprintf(fOut, "Licences on %s\n\n", cloud)
67 | if err != nil {
68 | return err
69 | }
70 |
71 | // Sort the licences by display order
72 | var licOrder displayOrderSlice
73 | for i, j := range licList {
74 | licOrder = append(licOrder, displayOrder{key: i, order: j.Order})
75 | }
76 | sort.Sort(displayOrderSlice(licOrder))
77 |
78 | // Display the licences
79 | for _, j := range licOrder {
80 | astShown := false
81 | if n := licList[j.key].FullName; n != "" {
82 | _, err = fmt.Fprintf(fOut, " * Full name: %s\n", n)
83 | if err != nil {
84 | return err
85 | }
86 | astShown = true
87 | }
88 |
89 | // Include the asterisk if the Full Name line wasn't displayed
90 | if astShown {
91 | _, err = fmt.Fprintf(fOut, " ")
92 | if err != nil {
93 | return err
94 | }
95 | } else {
96 | _, err = fmt.Fprintf(fOut, " * ")
97 | if err != nil {
98 | return err
99 | }
100 | astShown = true
101 | }
102 | _, err = fmt.Fprintf(fOut, "ID: %s\n", j.key)
103 | if err != nil {
104 | return err
105 | }
106 |
107 | if s := licList[j.key].URL; s != "" {
108 | _, err = fmt.Fprintf(fOut, " Source URL: %s\n", s)
109 | if err != nil {
110 | return err
111 | }
112 | }
113 | if licenceListDisplayOrder {
114 | _, err = fmt.Fprintf(fOut, " Display order: %d\n", licList[j.key].Order)
115 | if err != nil {
116 | return err
117 | }
118 | }
119 | _, err = fmt.Fprintf(fOut, " SHA256: %s\n\n", licList[j.key].Sha256)
120 | if err != nil {
121 | return err
122 | }
123 | }
124 | return nil
125 | }
126 |
--------------------------------------------------------------------------------
/cmd/licenceRemove.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 | "net/url"
8 |
9 | rq "github.com/parnurzeal/gorequest"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | // Removes a licence from the system.
14 | var licenceRemoveCmd = &cobra.Command{
15 | Use: "remove [licence name]",
16 | Short: "Removes a licence from the list of known licences on the server",
17 | RunE: func(cmd *cobra.Command, args []string) error {
18 | return licenceRemove(args)
19 | },
20 | }
21 |
22 | func init() {
23 | licenceCmd.AddCommand(licenceRemoveCmd)
24 | }
25 |
26 | func licenceRemove(args []string) error {
27 | // Ensure a licence friendly name is present
28 | if len(args) == 0 {
29 | return errors.New("A short licence name or identified is needed. eg CC0-BY-1.0")
30 | }
31 | if len(args) > 1 {
32 | return errors.New("Only one licence can be removed at a time (for now)")
33 | }
34 |
35 | // Remove the licence
36 | name := args[0]
37 | resp, body, errs := rq.New().TLSClientConfig(&TLSConfig).Post(fmt.Sprintf("%s/licence/remove", cloud)).
38 | Query(fmt.Sprintf("licence_id=%s", url.QueryEscape(name))).
39 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)).
40 | End()
41 | if errs != nil {
42 | _, err := fmt.Fprint(fOut, "Errors when removing licence:")
43 | if err != nil {
44 | return err
45 | }
46 | for _, err := range errs {
47 | _, errInner := fmt.Fprint(fOut, err.Error())
48 | if errInner != nil {
49 | return errInner
50 | }
51 | }
52 | return errors.New("Error when removing licence")
53 | }
54 | if resp.StatusCode != http.StatusOK {
55 | return errors.New(body)
56 | }
57 |
58 | _, err := fmt.Fprintf(fOut, "Licence '%s' removed\n", name)
59 | return err
60 | }
61 |
--------------------------------------------------------------------------------
/cmd/list.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | // Displays the list of databases on DBHub.io for the user.
11 | var listCmd = &cobra.Command{
12 | Use: "list",
13 | Short: "Returns the list of your databases on DBHub.io",
14 | RunE: func(cmd *cobra.Command, args []string) error {
15 | return list(args)
16 | },
17 | }
18 |
19 | func init() {
20 | RootCmd.AddCommand(listCmd)
21 | }
22 |
23 | func list(args []string) error {
24 | // TODO: Include things like # stars and fork count too
25 | // TODO: Add parameter for listing the (public) databases of other user(s) too
26 |
27 | // Retrieve the database list for the user
28 | dbList, err := getDatabases(cloud, certUser)
29 | if err != nil {
30 | return err
31 | }
32 |
33 | // Display the list of databases
34 | if len(dbList) == 0 {
35 | _, err = fmt.Fprintf(fOut, "Cloud '%s' has no databases\n", cloud)
36 | return err
37 | }
38 | fmt.Printf("Databases on %s\n\n", cloud)
39 | for _, j := range dbList {
40 | _, err = fmt.Fprintf(fOut, " * Database: %s\n", j.Name)
41 | if err != nil {
42 | return err
43 | }
44 | if j.OneLineDesc != "" {
45 | _, err = fmt.Fprintf(fOut, " Description: %s\n", j.OneLineDesc)
46 | if err != nil {
47 | return err
48 | }
49 | }
50 | _, err = fmt.Fprintf(fOut, " Default branch: %s\n", j.DefBranch)
51 | if err != nil {
52 | return err
53 | }
54 | _, err := numFormat.Fprintf(fOut, " Size: %d bytes\n", j.Size)
55 | if err != nil {
56 | return err
57 | }
58 | if j.Licence != "" {
59 | _, err = fmt.Fprintf(fOut, " Licence: %s\n", j.Licence)
60 | if err != nil {
61 | return err
62 | }
63 | } else {
64 | _, err = fmt.Fprintf(fOut, " Licence: Not specified")
65 | if err != nil {
66 | return err
67 | }
68 | }
69 | // The server gives us the last modified and repo modified dates in pre-formatted UTC timezone. For now, lets
70 | // convert these back to the users local time
71 | z, err := time.Parse(time.RFC3339, j.LastModified)
72 | if err != nil {
73 | return err
74 | }
75 | _, err = fmt.Fprintf(fOut, " File last modified: %s\n", z.Local().Format(time.RFC1123))
76 | if err != nil {
77 | return err
78 | }
79 | z, err = time.Parse(time.RFC3339, j.RepoModified)
80 | if err != nil {
81 | return err
82 | }
83 | _, err = fmt.Fprintf(fOut, " Repository last updated: %s\n\n", z.Local().Format(time.RFC1123))
84 | if err != nil {
85 | return err
86 | }
87 | }
88 | return nil
89 | }
90 |
--------------------------------------------------------------------------------
/cmd/log.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | var logBranch string
12 |
13 | // Retrieves the commit history for a database branch
14 | var branchLogCmd = &cobra.Command{
15 | Use: "log [database name]",
16 | Short: "Displays the history for a database branch",
17 | RunE: func(cmd *cobra.Command, args []string) error {
18 | return branchLog(args)
19 | },
20 | }
21 |
22 | func init() {
23 | RootCmd.AddCommand(branchLogCmd)
24 | branchLogCmd.Flags().StringVar(&logBranch, "branch", "", "Remote branch to retrieve the "+
25 | "history of")
26 | }
27 |
28 | func branchLog(args []string) error {
29 | // Ensure a database file was given
30 | var db string
31 | var err error
32 | if len(args) == 0 {
33 | db, err = getDefaultDatabase()
34 | if err != nil {
35 | return err
36 | }
37 | if db == "" {
38 | // No database name was given on the command line, and we don't have a default database selected
39 | return errors.New("No database file specified")
40 | }
41 | } else {
42 | db = args[0]
43 | }
44 | if len(args) > 1 {
45 | return errors.New("only one database can be worked with at a time (for now)")
46 | }
47 |
48 | // If there is a local metadata cache for the requested database, use that. Otherwise, retrieve it from the
49 | // server first (without storing it)
50 | var meta metaData
51 | meta, err = localFetchMetadata(db, true)
52 | if err != nil {
53 | return err
54 | }
55 |
56 | // If a branch name was given by the user, check if it exists
57 | if logBranch != "" {
58 | if _, ok := meta.Branches[logBranch]; ok == false {
59 | return errors.New("That branch doesn't exist for the database")
60 | }
61 | } else {
62 | logBranch = meta.ActiveBranch
63 | }
64 |
65 | // Retrieve the list of known licences
66 | l, err := getLicences()
67 | if err != nil {
68 | return err
69 | }
70 |
71 | // Map the license sha256's to their friendly name for easy lookup
72 | licList := make(map[string]string)
73 | for _, j := range l {
74 | licList[j.Sha256] = j.FullName
75 | }
76 |
77 | // Display the commits for the branch
78 | headID := meta.Branches[logBranch].Commit
79 | localCommit := meta.Commits[headID]
80 | _, err = fmt.Fprintf(fOut, "Branch \"%s\" history for %s:\n\n", logBranch, db)
81 | if err != nil {
82 | return err
83 | }
84 | _, err = fmt.Fprint(fOut, createCommitText(meta.Commits[localCommit.ID], licList))
85 | if err != nil {
86 | return err
87 | }
88 | for localCommit.Parent != "" {
89 | localCommit = meta.Commits[localCommit.Parent]
90 | _, err = fmt.Fprintf(fOut, createCommitText(meta.Commits[localCommit.ID], licList))
91 | if err != nil {
92 | return err
93 | }
94 | }
95 | return nil
96 | }
97 |
98 | // Creates the user visible commit text for a commit.
99 | func createCommitText(c commitEntry, licList map[string]string) string {
100 | s := fmt.Sprintf(" * Commit: %s\n", c.ID)
101 | s += fmt.Sprintf(" Author: %s <%s>\n", c.AuthorName, c.AuthorEmail)
102 | s += fmt.Sprintf(" Date: %v\n", c.Timestamp.Local().Format(time.RFC1123))
103 | if c.Tree.Entries[0].LicenceSHA != "" {
104 | s += fmt.Sprintf(" Licence: %s\n\n", licList[c.Tree.Entries[0].LicenceSHA])
105 | } else {
106 | s += fmt.Sprintf("\n")
107 | }
108 | if c.Message != "" {
109 | s += fmt.Sprintf(" %s\n\n", c.Message)
110 | }
111 | return s
112 | }
113 |
--------------------------------------------------------------------------------
/cmd/pull.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "crypto/sha256"
5 | "encoding/hex"
6 | "fmt"
7 | "io/ioutil"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 | "time"
12 |
13 | "github.com/pkg/errors"
14 | "github.com/spf13/cobra"
15 | )
16 |
17 | var (
18 | pullCmdBranch, pullCmdCommit string
19 | pullForce *bool
20 | )
21 |
22 | // Downloads a database from DBHub.io.
23 | var pullCmd = &cobra.Command{
24 | Use: "pull [database name]",
25 | Short: "Download a database from DBHub.io",
26 | RunE: func(cmd *cobra.Command, args []string) error {
27 | return pull(args)
28 | },
29 | }
30 |
31 | func init() {
32 | RootCmd.AddCommand(pullCmd)
33 | pullCmd.Flags().StringVar(&pullCmdBranch, "branch", "",
34 | "Remote branch the database will be downloaded from")
35 | pullCmd.Flags().StringVar(&pullCmdCommit, "commit", "",
36 | "Commit ID of the database to download")
37 | pullForce = pullCmd.Flags().BoolP("force", "f", false,
38 | "Overwrite unsaved changes to the database?")
39 | }
40 |
41 | func pull(args []string) error {
42 | // Ensure a database file was given
43 | var db, defDB string
44 | var err error
45 | if len(args) == 0 {
46 | db, err = getDefaultDatabase()
47 | if err != nil {
48 | return err
49 | }
50 | if db == "" {
51 | // No database name was given on the command line, and we don't have a default database selected
52 | return errors.New("No database file specified")
53 | }
54 | } else {
55 | db = args[0]
56 | }
57 |
58 | // TODO: Allow giving multiple database files on the command line. Hopefully just needs turning this
59 | // TODO into a for loop
60 | if len(args) > 1 {
61 | return errors.New("Only one database can be downloaded at a time (for now)")
62 | }
63 |
64 | // TODO: Add a --licence option, for automatically grabbing the licence as well
65 | // * Probably save it as -.txt/html
66 |
67 | // Ensure we weren't given potentially conflicting info on what to pull down
68 | if pullCmdBranch != "" && pullCmdCommit != "" {
69 | return errors.New("Either a branch name or commit ID can be given. Not both at the same time!")
70 | }
71 |
72 | // Retrieve metadata for the database
73 | var meta metaData
74 | meta, err = updateMetadata(db, false) // Don't store the metadata to disk yet, in case the download fails
75 | if err != nil {
76 | return err
77 | }
78 |
79 | // If the database file already exists locally, check whether the file has changed since the last commit, and let
80 | // the user know. The --force option on the command line overrides this
81 | if _, err = os.Stat(db); err == nil {
82 | if *pullForce == false {
83 | changed, err := dbChanged(db, meta)
84 | if err != nil {
85 | return err
86 | }
87 | if changed {
88 | _, err = fmt.Fprintf(fOut, "%s has been changed since the last commit. Use --force if you "+
89 | "really want to overwrite it\n", db)
90 | return err
91 | }
92 | }
93 | }
94 |
95 | // If given, make sure the requested branch exists
96 | if pullCmdBranch != "" {
97 | if _, ok := meta.Branches[pullCmdBranch]; ok == false {
98 | return errors.New("The requested branch doesn't exist")
99 | }
100 | }
101 |
102 | // If no specific branch nor commit were requested, we use the active branch set in the metadata
103 | if pullCmdBranch == "" && pullCmdCommit == "" {
104 | pullCmdBranch = meta.ActiveBranch
105 | }
106 |
107 | // If given, make sure the requested commit exists
108 | var lastMod time.Time
109 | var ok bool
110 | var thisSha string
111 | var thisCommit commitEntry
112 | if pullCmdCommit != "" {
113 | thisCommit, ok = meta.Commits[pullCmdCommit]
114 | if ok == false {
115 | return errors.New("The requested commit doesn't exist")
116 | }
117 | thisSha = thisCommit.Tree.Entries[0].Sha256
118 | lastMod = thisCommit.Tree.Entries[0].LastModified
119 | } else {
120 | // Determine the sha256 of the database file
121 | c := meta.Branches[pullCmdBranch].Commit
122 | thisCommit, ok = meta.Commits[c]
123 | if ok == false {
124 | return errors.New("The requested commit doesn't exist")
125 | }
126 | thisSha = thisCommit.Tree.Entries[0].Sha256
127 | lastMod = thisCommit.Tree.Entries[0].LastModified
128 | }
129 |
130 | // Check if the database file already exists in local cache
131 | if thisSha != "" {
132 | if _, err = os.Stat(filepath.Join(".dio", db, "db", thisSha)); err == nil {
133 | // The database is already in the local cache, so use that instead of downloading from DBHub.io
134 | var b []byte
135 | b, err = ioutil.ReadFile(filepath.Join(".dio", db, "db", thisSha))
136 | if err != nil {
137 | return err
138 | }
139 | err = ioutil.WriteFile(db, b, 0644)
140 | if err != nil {
141 | return err
142 | }
143 | err = os.Chtimes(db, time.Now(), lastMod)
144 | if err != nil {
145 | return err
146 | }
147 |
148 | _, err = fmt.Fprintf(fOut, "Database '%s' refreshed from local cache\n", db)
149 | if err != nil {
150 | return err
151 | }
152 | if pullCmdBranch != "" {
153 | _, err = fmt.Fprintf(fOut, " * Branch: '%s'\n", pullCmdBranch)
154 | if err != nil {
155 | return err
156 | }
157 | }
158 | if pullCmdCommit != "" {
159 | _, err = fmt.Fprintf(fOut, " * Commit: %s\n", pullCmdCommit)
160 | if err != nil {
161 | return err
162 | }
163 | }
164 | _, err = numFormat.Fprintf(fOut, " * Size: %d bytes\n", len(b))
165 | if err != nil {
166 | return err
167 | }
168 |
169 | // Update the branch metadata with the commit info
170 | var oldBranch branchEntry
171 | if pullCmdBranch == "" {
172 | oldBranch = meta.Branches[meta.ActiveBranch]
173 | } else {
174 | oldBranch = meta.Branches[pullCmdBranch]
175 | }
176 | commitCount := 1
177 | z := meta.Commits[thisCommit.ID]
178 | for z.Parent != "" {
179 | commitCount++
180 | z = meta.Commits[z.Parent]
181 | }
182 | newBranch := branchEntry{
183 | Commit: thisCommit.ID,
184 | CommitCount: commitCount,
185 | Description: oldBranch.Description,
186 | }
187 | if pullCmdBranch == "" {
188 | meta.Branches[meta.ActiveBranch] = newBranch
189 | } else {
190 | meta.Branches[pullCmdBranch] = newBranch
191 | }
192 |
193 | // Save the updated metadata to disk
194 | err = saveMetadata(db, meta)
195 | if err != nil {
196 | return err
197 | }
198 |
199 | // If a default database isn't already selected, we use this one as the default
200 | defDB, err = getDefaultDatabase()
201 | if err != nil {
202 | return err
203 | }
204 | if defDB == "" {
205 | err = saveDefaultDatabase(db)
206 | if err != nil {
207 | return err
208 | }
209 | }
210 | return nil
211 | }
212 | }
213 |
214 | // Download the database file
215 | // TODO: Use a streaming download approach, so download progress can be shown. Something like this should help:
216 | // https://stackoverflow.com/questions/22108519/how-do-i-read-a-streaming-response-body-using-golangs-net-http-package
217 | _, err = fmt.Fprintf(fOut, "Downloading '%s' from %s...\n", db, cloud)
218 | if err != nil {
219 | return err
220 | }
221 | resp, body, err := retrieveDatabase(db, pullCmdBranch, pullCmdCommit)
222 | if err != nil {
223 | return err
224 | }
225 |
226 | // Create the local database cache directory, if it doesn't yet exist
227 | if _, err = os.Stat(filepath.Join(".dio", db, "db")); os.IsNotExist(err) {
228 | err = os.MkdirAll(filepath.Join(".dio", db, "db"), 0770)
229 | if err != nil {
230 | return err
231 | }
232 | }
233 |
234 | // Calculate the sha256 of the database file
235 | s := sha256.Sum256(body)
236 | shaSum := hex.EncodeToString(s[:])
237 |
238 | // Write the database file to disk in the cache directory
239 | err = ioutil.WriteFile(filepath.Join(".dio", db, "db", shaSum), body, 0644)
240 | if err != nil {
241 | return err
242 | }
243 |
244 | // Write the database file to disk again, this time in the working directory
245 | err = ioutil.WriteFile(db, body, 0644)
246 | if err != nil {
247 | return err
248 | }
249 |
250 | // If the headers included the modification-date parameter for the database, set the last accessed and last
251 | // modified times on the new database file
252 | if disp := resp.Header.Get("Content-Disposition"); disp != "" {
253 | s := strings.Split(disp, ";")
254 | if len(s) == 4 {
255 | a := strings.TrimLeft(s[2], " ")
256 | if strings.HasPrefix(a, "modification-date=") {
257 | b := strings.Split(a, "=")
258 | c := strings.Trim(b[1], "\"")
259 | lastMod, err := time.Parse(time.RFC3339, c)
260 | if err != nil {
261 | return err
262 | }
263 | err = os.Chtimes(db, time.Now(), lastMod)
264 | if err != nil {
265 | return err
266 | }
267 | }
268 | }
269 | }
270 |
271 | // If the server provided a branch name, add it to the local metadata cache
272 | if branch := resp.Header.Get("Branch"); branch != "" {
273 | meta.ActiveBranch = branch
274 | }
275 |
276 | // The download succeeded, so save the updated metadata to disk
277 | err = saveMetadata(db, meta)
278 | if err != nil {
279 | return err
280 | }
281 |
282 | // If a default database isn't already selected, we use this one as the default
283 | defDB, err = getDefaultDatabase()
284 | if err != nil {
285 | return err
286 | }
287 | if defDB == "" {
288 | err = saveDefaultDatabase(db)
289 | if err != nil {
290 | return err
291 | }
292 | }
293 |
294 | // Display success message to the user
295 | comID := resp.Header.Get("Commit-Id")
296 | _, err = fmt.Fprintln(fOut, "Downloaded complete")
297 | if err != nil {
298 | return err
299 | }
300 | if pullCmdBranch != "" {
301 | _, err = fmt.Fprintf(fOut, " * Branch: '%s'\n", pullCmdBranch)
302 | if err != nil {
303 | return err
304 | }
305 | }
306 | if comID != "" {
307 | _, err = fmt.Fprintf(fOut, " * Commit: %s\n", comID)
308 | if err != nil {
309 | return err
310 | }
311 | }
312 | _, err = numFormat.Fprintf(fOut, " * Size: %d bytes\n", len(body))
313 | return err
314 | }
315 |
--------------------------------------------------------------------------------
/cmd/push.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "crypto/sha256"
5 | "encoding/hex"
6 | "encoding/json"
7 | "fmt"
8 | "io/ioutil"
9 | "log"
10 | "net/http"
11 | "net/url"
12 | "os"
13 | "path/filepath"
14 | "time"
15 |
16 | rq "github.com/parnurzeal/gorequest"
17 | "github.com/pkg/errors"
18 | "github.com/spf13/cobra"
19 | "github.com/spf13/viper"
20 | )
21 |
22 | var (
23 | pushCmdBranch, pushCmdCommit, pushCmdDB string
24 | pushCmdEmail, pushCmdLicence, pushCmdMsg string
25 | pushCmdName, pushCmdTimestamp string
26 | pushCmdForce, pushCmdPublic bool
27 | )
28 |
29 | // Uploads a database to DBHub.io.
30 | var pushCmd = &cobra.Command{
31 | Use: "push [database file]",
32 | Short: "Upload a database",
33 | RunE: func(cmd *cobra.Command, args []string) error {
34 | return push(args)
35 | },
36 | }
37 |
38 | func init() {
39 | RootCmd.AddCommand(pushCmd)
40 | pushCmd.Flags().StringVar(&pushCmdName, "author", "", "Author name")
41 | pushCmd.Flags().StringVar(&pushCmdBranch, "branch", "",
42 | "Remote branch the database will be uploaded to")
43 | pushCmd.Flags().StringVar(&pushCmdCommit, "commit", "",
44 | "ID of the previous commit, for appending this new database to")
45 | pushCmd.Flags().StringVar(&pushCmdDB, "dbname", "", "Override for the database name")
46 | pushCmd.Flags().StringVar(&pushCmdEmail, "email", "", "Email address of the author")
47 | pushCmd.Flags().BoolVar(&pushCmdForce, "force", false, "Overwrite existing commit history?")
48 | pushCmd.Flags().StringVar(&pushCmdLicence, "licence", "",
49 | "The licence (ID) for the database, as per 'dio licence list'")
50 | pushCmd.Flags().StringVar(&pushCmdMsg, "message", "",
51 | "(Required) Commit message for this upload")
52 | pushCmd.Flags().BoolVar(&pushCmdPublic, "public", false, "Should the database be public?")
53 | pushCmd.Flags().StringVar(&pushCmdTimestamp, "timestamp", "", "Timestamp to use as the commit date")
54 | }
55 |
56 | func push(args []string) error {
57 | // Ensure a database file was given
58 | var db string
59 | var err error
60 | if len(args) == 0 {
61 | db, err = getDefaultDatabase()
62 | if err != nil {
63 | return err
64 | }
65 | if db == "" {
66 | // No database name was given on the command line, and we don't have a default database selected
67 | return errors.New("No database file specified")
68 | }
69 | } else {
70 | db = args[0]
71 | }
72 | // TODO: Allow giving multiple database files on the command line. Hopefully just needs turning this
73 | // TODO into a for loop
74 | if len(args) > 1 {
75 | return errors.New("Only one database can be uploaded at a time (for now)")
76 | }
77 |
78 | // Ensure the database file exists
79 | fi, err := os.Stat(db)
80 | if err != nil {
81 | return err
82 | }
83 |
84 | // Grab author name & email from the dio config file, but allow command line flags to override them
85 | var committerName, committerEmail, pushAuthor, pushEmail string
86 | u, ok := viper.Get("user.name").(string)
87 | if ok {
88 | pushAuthor = u
89 | committerName = u
90 | }
91 | v, ok := viper.Get("user.email").(string)
92 | if ok {
93 | pushEmail = v
94 | committerEmail = u
95 | }
96 | if pushCmdName != "" {
97 | pushAuthor = pushCmdName
98 | }
99 | if pushCmdEmail != "" {
100 | pushEmail = pushCmdEmail
101 | }
102 |
103 | // Author name and email are required
104 | if pushAuthor == "" || pushEmail == "" {
105 | return errors.New("Both author name and email are required!")
106 | }
107 |
108 | // Determine name to store database as
109 | if pushCmdDB == "" {
110 | pushCmdDB = filepath.Base(db)
111 | }
112 |
113 | // Check if there's local metadata. If there is, we compare the local branch metadata with that on the server.
114 | // Then we go through a simple loop, uploading each outstanding commit to the remote server along with it's
115 | // metadata (via appropriate http headers)
116 | var meta metaData
117 | dbURL := fmt.Sprintf("%s/%s/%s", cloud, certUser, db)
118 | if _, err = os.Stat(filepath.Join(".dio", db, "metadata.json")); err == nil {
119 | // Load the local metadata cache, without retrieving updated metadata from the cloud
120 | meta, err = localFetchMetadata(db, false)
121 | if err != nil {
122 | return err
123 | }
124 |
125 | // If no branch name was given on the command line, we use the active branch
126 | if pushCmdBranch == "" {
127 | pushCmdBranch = meta.ActiveBranch
128 | }
129 |
130 | // Check the branch exists locally
131 | localHead, ok := meta.Branches[pushCmdBranch]
132 | if !ok {
133 | return errors.New(fmt.Sprintf("That branch ('%s') doesn't exist", pushCmdBranch))
134 | }
135 |
136 | // Build a list of the commits in the local branch
137 | localCommitList := []string{localHead.Commit}
138 | c, ok := meta.Commits[localHead.Commit]
139 | if ok == false {
140 | return errors.New("Something has gone wrong. Head commit for the local branch isn't in the " +
141 | "local commit list")
142 | }
143 | for c.Parent != "" {
144 | c = meta.Commits[c.Parent]
145 | localCommitList = append(localCommitList, c.ID)
146 | }
147 | localCommitLength := len(localCommitList) - 1
148 |
149 | // Download the latest database metadata
150 | extraCtr := 0
151 | newMeta, found, err := retrieveMetadata(db)
152 | if err != nil {
153 | return err
154 | }
155 | if !found {
156 | // The database only exists locally, so we use the first commit to create the remote database,
157 | // then loop around pushing the remaining commits
158 | newCommit := meta.Commits[localCommitList[len(localCommitList)-1]].ID
159 | err = sendCommit(meta, db, dbURL, newCommit, pushCmdPublic)
160 | if err != nil {
161 | return err
162 | }
163 |
164 | // If there was only a single commit to push, there's nothing more to do
165 | if len(localCommitList) == 1 {
166 | _, err = fmt.Fprintf(fOut, "Database uploaded to %s\n\n", cloud)
167 | if err != nil {
168 | return err
169 | }
170 | _, err = fmt.Fprintf(fOut, " * Name: %s\n", pushCmdDB)
171 | if err != nil {
172 | return err
173 | }
174 | _, err = fmt.Fprintf(fOut, " Branch: %s\n", pushCmdBranch)
175 | if err != nil {
176 | return err
177 | }
178 | if pushCmdLicence != "" {
179 | _, err = fmt.Fprintf(fOut, " Licence: %s\n", pushCmdLicence)
180 | if err != nil {
181 | return err
182 | }
183 | }
184 | _, err = numFormat.Fprintf(fOut, " Size: %d bytes\n", fi.Size())
185 | if err != nil {
186 | return err
187 | }
188 | if pushCmdMsg != "" {
189 | _, err = fmt.Fprintf(fOut, " Commit message: %s\n", pushCmdMsg)
190 | if err != nil {
191 | return err
192 | }
193 | }
194 | _, err = fmt.Fprintln(fOut)
195 | return err
196 | }
197 |
198 | // Let the user know the remote database has been created
199 | _, err = fmt.Fprintf(fOut, "Created new database '%s' on %s\n", db, cloud)
200 | if err != nil {
201 | return err
202 | }
203 |
204 | // Fetch the remote metadata, now that the database exists remotely. This lets us use the existing
205 | // code below to add the remaining commits
206 | newMeta, found, err = retrieveMetadata(db)
207 | if err != nil {
208 | return err
209 | }
210 | extraCtr++
211 | }
212 |
213 | // * To get here, the database exists on the remote cloud and has local metadata *
214 |
215 | // Check the branch exists remotely
216 | remoteHead, ok := newMeta.Branches[pushCmdBranch]
217 | if !ok {
218 | // * The branch doesn't exist remotely, so create a fork on the remote cloud *
219 |
220 | // Determine which of the commits in the local branch is the first one not also on the server
221 | extraCtr++
222 | var baseBranchCounter int
223 | remoteBranchCommitCounter := make(map[string]int)
224 | for brName, brEntry := range newMeta.Branches {
225 | // Build a list of the commits in the remote branch
226 | remoteBranchCommitCounter[brName] = 0
227 | remoteCommitList := make(map[string]struct{})
228 | remoteCommitList[brEntry.Commit] = struct{}{}
229 | c, ok = newMeta.Commits[brEntry.Commit]
230 | if ok == false {
231 | return errors.New("Something has gone wrong. Head commit for the remote branch " +
232 | "isn't in the remote commit list")
233 | }
234 | for c.Parent != "" {
235 | c = newMeta.Commits[c.Parent]
236 | remoteCommitList[c.ID] = struct{}{}
237 | }
238 |
239 | // At this point we have both a local and remote commit list, so we can now compare them and count
240 | // the # of matches for this branch
241 | for _, j := range localCommitList {
242 | if _, ok := remoteCommitList[j]; ok {
243 | remoteBranchCommitCounter[brName]++
244 | }
245 | }
246 | }
247 |
248 | // We take the highest number of known commits here, as that means the next commit in line is the first
249 | // unknown one on the remote cloud
250 | for _, j := range remoteBranchCommitCounter {
251 | if j > baseBranchCounter {
252 | baseBranchCounter = j
253 | }
254 | }
255 |
256 | // Create the new (forked) branch on DBHub.io
257 | newCommit := localCommitList[localCommitLength-baseBranchCounter]
258 | err = sendCommit(meta, db, dbURL, newCommit, pushCmdPublic)
259 | if err != nil {
260 | return err
261 | }
262 |
263 | // Count the number of commits in the new fork
264 | d := meta.Commits[newCommit]
265 | forkCommitCtr := 1
266 | for d.Parent != "" {
267 | d = meta.Commits[d.Parent]
268 | forkCommitCtr++
269 | }
270 |
271 | // Add the new (forked) branch to the local list of remote metadata
272 | newMeta.Branches[pushCmdBranch] = branchEntry{
273 | Commit: newCommit,
274 | CommitCount: forkCommitCtr,
275 | Description: meta.Branches[pushCmdBranch].Description,
276 | }
277 | remoteHead = newMeta.Branches[pushCmdBranch]
278 |
279 | // Add the newly generated commit to the local list of remote metadata
280 | newMeta.Commits[newCommit] = meta.Commits[newCommit]
281 |
282 | // If this fork only had the one commit (eg no further commits to push), then finish here
283 | if len(localCommitList) == forkCommitCtr {
284 | _, err = fmt.Fprintf(fOut, "New branch '%s' created and all commits for it pushed to %s\n",
285 | pushCmdBranch, cloud)
286 | return err
287 | }
288 |
289 | // * Now that the initial commit for the new branch is on the remote server, we can continue on
290 | // "as per normal", using the existing code to loop around adding the remaining commits *
291 | }
292 |
293 | // Build a list of the commits in the remote branch
294 | remoteCommitList := []string{remoteHead.Commit}
295 | c, ok = newMeta.Commits[remoteHead.Commit]
296 | if ok == false {
297 | return errors.New("Something has gone wrong. Head commit for the remote branch isn't in " +
298 | "the remote commit list")
299 | }
300 | for c.Parent != "" {
301 | c = newMeta.Commits[c.Parent]
302 | remoteCommitList = append(remoteCommitList, c.ID)
303 | }
304 | remoteCommitLength := len(remoteCommitList) - 1
305 |
306 | // Make sure the local and remote commits start out with the same commit ID
307 | if localCommitList[localCommitLength] != remoteCommitList[remoteCommitLength] {
308 | // The local and remote branches don't have a common root, so abort
309 | err = errors.New(fmt.Sprintf("Local and remote branch %s don't have a common root. "+
310 | "Aborting.", pushCmdBranch))
311 | return err
312 | }
313 |
314 | // * Compare the local branch to the head of the remote branch, to determine which commits need sending *
315 |
316 | // If there are more commits in the remote branch than in the local one, then the branches have diverged
317 | // so abort (for now).
318 | // TODO: Write the code to allow --force overwriting for this
319 | if remoteCommitLength > localCommitLength {
320 | return fmt.Errorf("The remote branch has more commits than the local one. Can't push the " +
321 | "branch. If you want to overwrite changes on the remote server, consider the --force option.")
322 | }
323 |
324 | // Check if the given branch is the same on the local and remote server. If it is, nothing needs to be done
325 | if remoteCommitLength == localCommitLength && remoteCommitList[0] == localCommitList[0] {
326 | return fmt.Errorf("The local and remote branch '%s' are identical. Nothing to push.",
327 | pushCmdBranch)
328 | }
329 |
330 | // * To get here, the local branch has more commits than the remote one *
331 |
332 | // Create the list of commits that need pushing
333 | var pushCommits []string
334 | for i := 0; i <= localCommitLength; i++ {
335 | lCommit := localCommitList[localCommitLength-i]
336 | if i > remoteCommitLength {
337 | pushCommits = append(pushCommits, lCommit)
338 | } else {
339 | rCommit := remoteCommitList[remoteCommitLength-i]
340 | if lCommit != rCommit {
341 | // There are conflicting commits in this branch between the local metadata and the
342 | // remote. Abort (for now)
343 | // TODO: Consider how to allow --force pushing here. Also remember that when doing this, there
344 | // needs a check added for potentially isolated tags and releases, same as branch revert
345 | e := fmt.Sprintf("The local and remote branch have conflicting commits.\n\n")
346 | e = fmt.Sprintf("%s * local commit: %s\n", e, lCommit)
347 | e = fmt.Sprintf("%s * remote commit: %s\n\n", e, rCommit)
348 | e = fmt.Sprintf("%sCan't push the branch. If you want to overwrite changes on the "+
349 | "remote server, consider the --force option.", e)
350 | return errors.New(e)
351 | }
352 | }
353 | }
354 |
355 | // Display useful info message to the user
356 | numCommits := len(pushCommits) + extraCtr
357 | if numCommits == 1 {
358 | _, err = fmt.Fprintf(fOut, "Pushing 1 commit for branch '%s'", pushCmdBranch)
359 | if err != nil {
360 | return err
361 | }
362 | } else {
363 | _, err = fmt.Fprintf(fOut, "Pushing %d commit(s) for branch '%s'", numCommits, pushCmdBranch)
364 | if err != nil {
365 | return err
366 | }
367 | }
368 | _, err = fmt.Fprintf(fOut, " to %s...\n", cloud)
369 | if err != nil {
370 | return err
371 | }
372 |
373 | // Send the commits to the cloud
374 | for _, commitID := range pushCommits {
375 | err = sendCommit(meta, db, dbURL, commitID, pushCmdPublic)
376 | if err != nil {
377 | return err
378 | }
379 | }
380 | _, err = fmt.Fprintln(fOut, "All commits pushed.")
381 | return err
382 | }
383 |
384 | // To get here, we don't have existing metadata. We just use the original file upload code, which creates the
385 | // database remotely (if it's not there already) and creates the local metadata.
386 | // If the database already exists remotely, this code will fail.
387 | // TODO: Maybe add a nicer failure message here for when local metadata is missing but the db exists remotely?
388 | z, ok := viper.Get("user.name").(string)
389 | if !ok {
390 | return fmt.Errorf("Committer name could not be determined")
391 | }
392 | committerName = z
393 | z, ok = viper.Get("user.email").(string)
394 | if !ok {
395 | return fmt.Errorf("Committer email could not be determined")
396 | }
397 | committerEmail = z
398 |
399 | b, err := ioutil.ReadFile(db)
400 | if err != nil {
401 | return err
402 | }
403 | s := sha256.Sum256(b)
404 | shaSum := hex.EncodeToString(s[:])
405 | req := rq.New().TLSClientConfig(&TLSConfig).Post(dbURL).
406 | Type("multipart").
407 | Query(fmt.Sprintf("authoremail=%s", url.QueryEscape(pushEmail))).
408 | Query(fmt.Sprintf("authorname=%s", url.QueryEscape(pushAuthor))).
409 | Query(fmt.Sprintf("branch=%s", url.QueryEscape(pushCmdBranch))).
410 | Query(fmt.Sprintf("commit=%s", pushCmdCommit)).
411 | Query(fmt.Sprintf("commitmsg=%s", url.QueryEscape(pushCmdMsg))).
412 | Query(fmt.Sprintf("committeremail=%s", url.QueryEscape(committerEmail))).
413 | Query(fmt.Sprintf("committername=%s", url.QueryEscape(committerName))).
414 | Query(fmt.Sprintf("committimestamp=%v", pushCmdTimestamp)).
415 | Query(fmt.Sprintf("dbshasum=%s", url.QueryEscape(shaSum))).
416 | Query(fmt.Sprintf("force=%v", pushCmdForce)).
417 | Query(fmt.Sprintf("lastmodified=%s", url.QueryEscape(fi.ModTime().UTC().Format(time.RFC3339)))).
418 | Query(fmt.Sprintf("public=%v", pushCmdPublic)).
419 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)).
420 | SendFile(db, "", "file1")
421 | if pushCmdLicence != "" {
422 | req.Query(fmt.Sprintf("licence=%s", url.QueryEscape(pushCmdLicence)))
423 | }
424 | resp, _, errs := req.End()
425 | if errs != nil {
426 | log.Print("Errors when uploading database to the cloud:")
427 | for _, err := range errs {
428 | _, _ = fmt.Fprint(fOut, err)
429 | }
430 | return errors.New("Error when uploading database to the cloud")
431 | }
432 | if resp != nil && resp.StatusCode != http.StatusCreated {
433 | return errors.New(fmt.Sprintf("Upload failed with an error: HTTP status %d - '%v'\n",
434 | resp.StatusCode, resp.Status))
435 | }
436 |
437 | // Retrieve updated metadata
438 | meta, _, err = retrieveMetadata(db)
439 | if err != nil {
440 | return err
441 | }
442 | meta.ActiveBranch = meta.DefBranch
443 | if pushCmdBranch == "" {
444 | pushCmdBranch = meta.ActiveBranch
445 | }
446 |
447 | // Save the updated metadata back to disk
448 | err = saveMetadata(db, meta)
449 | if err != nil {
450 | return err
451 | }
452 |
453 | // If the database isn't in the local metadata cache, then copy it there
454 | err = ioutil.WriteFile(filepath.Join(".dio", db, "db", shaSum), b, 0644)
455 | if err != nil {
456 | return err
457 | }
458 |
459 | _, err = fmt.Fprintf(fOut, "Database uploaded to %s\n\n", cloud)
460 | if err != nil {
461 | return err
462 | }
463 | _, err = fmt.Fprintf(fOut, " * Name: %s\n", pushCmdDB)
464 | if err != nil {
465 | return err
466 | }
467 | _, err = fmt.Fprintf(fOut, " Branch: %s\n", pushCmdBranch)
468 | if err != nil {
469 | return err
470 | }
471 | if pushCmdLicence != "" {
472 | _, err = fmt.Fprintf(fOut, " Licence: %s\n", pushCmdLicence)
473 | if err != nil {
474 | return err
475 | }
476 | }
477 | _, err = numFormat.Fprintf(fOut, " Size: %d bytes\n", fi.Size())
478 | if err != nil {
479 | _, errInner := fmt.Fprintln(fOut)
480 | if errInner != nil {
481 | return fmt.Errorf("%s: %s", err, errInner)
482 | }
483 | return err
484 | }
485 | if pushCmdMsg != "" {
486 | _, err = fmt.Fprintf(fOut, " Commit message: %s\n", pushCmdMsg)
487 | if err != nil {
488 | return err
489 | }
490 | }
491 | _, err = fmt.Fprintln(fOut)
492 | return err
493 | }
494 |
495 | // Sends a commit to the cloud
496 | func sendCommit(meta metaData, db string, dbURL string, newCommit string, public bool) (err error) {
497 | commitData, ok := meta.Commits[newCommit]
498 | if !ok {
499 | return fmt.Errorf("Something went wrong. Could not retrieve data for commit '%s' from"+
500 | "local metadata commit list.", newCommit)
501 | }
502 | shaSum := commitData.Tree.Entries[0].Sha256
503 | var otherParents string
504 | for i, j := range commitData.OtherParents {
505 | if i != 1 {
506 | otherParents += ","
507 | }
508 | otherParents += j
509 | }
510 |
511 | // Push the first commit to the remote cloud, to create the database there
512 | req := rq.New().TLSClientConfig(&TLSConfig).Post(dbURL).
513 | Type("multipart").
514 | Query(fmt.Sprintf("branch=%s", url.QueryEscape(pushCmdBranch))).
515 | Query(fmt.Sprintf("commitmsg=%s", url.QueryEscape(commitData.Message))).
516 | Query(fmt.Sprintf("lastmodified=%s",
517 | url.QueryEscape(commitData.Tree.Entries[0].LastModified.UTC().Format(time.RFC3339)))).
518 | Query(fmt.Sprintf("commit=%s", commitData.Parent)).
519 | Query(fmt.Sprintf("authoremail=%s", url.QueryEscape(commitData.AuthorEmail))).
520 | Query(fmt.Sprintf("authorname=%s", url.QueryEscape(commitData.AuthorName))).
521 | Query(fmt.Sprintf("committeremail=%s", url.QueryEscape(commitData.CommitterEmail))).
522 | Query(fmt.Sprintf("committername=%s", url.QueryEscape(commitData.CommitterName))).
523 | Query(fmt.Sprintf("committimestamp=%s",
524 | url.QueryEscape(commitData.Timestamp.UTC().Format(time.RFC3339)))).
525 | Query(fmt.Sprintf("otherparents=%s", url.QueryEscape(otherParents))).
526 | Query(fmt.Sprintf("dbshasum=%s", url.QueryEscape(shaSum))).
527 | Query(fmt.Sprintf("public=%v", pushCmdPublic)).
528 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)).
529 | SendFile(filepath.Join(".dio", db, "db", shaSum), db, "file1")
530 | if pushCmdLicence != "" {
531 | req.Query(fmt.Sprintf("licence=%s", url.QueryEscape(pushCmdLicence)))
532 | }
533 | resp, body, errs := req.End()
534 | if errs != nil {
535 | e := fmt.Sprintln("Errors when uploading database to the cloud:")
536 | for _, err := range errs {
537 | e = err.Error()
538 | }
539 | return errors.New(e)
540 | }
541 | if resp != nil && resp.StatusCode != http.StatusCreated {
542 | return errors.New(fmt.Sprintf("Upload failed with an error: '%v'", body))
543 | }
544 |
545 | // Process the JSON format response data
546 | parsedResponse := map[string]string{}
547 | err = json.Unmarshal([]byte(body), &parsedResponse)
548 | if err != nil {
549 | _, errInner := fmt.Fprintf(fOut, "Error parsing server response: '%v'", err.Error())
550 | if errInner != nil {
551 | return fmt.Errorf("%s: %s", err, errInner)
552 | }
553 | return err
554 | }
555 |
556 | // Check that the ID for the new commit as generated by the server matches the ID generated locally
557 | remoteCommitID, ok := parsedResponse["commit_id"]
558 | if !ok {
559 | return errors.New("Unexpected response from server, doesn't contain new commit ID.")
560 | }
561 | if remoteCommitID != newCommit {
562 | return fmt.Errorf("Error. The Commit ID generated on the server (%s) doesn't match the "+
563 | "local Commit ID (%s)", remoteCommitID, newCommit)
564 | }
565 | return
566 | }
567 |
--------------------------------------------------------------------------------
/cmd/release.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | var releaseCmd = &cobra.Command{
8 | Use: "release",
9 | Short: "Create, list and remove releases for a database",
10 | }
11 |
12 | func init() {
13 | RootCmd.AddCommand(releaseCmd)
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/releaseCreate.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "time"
8 |
9 | "github.com/spf13/cobra"
10 | "github.com/spf13/viper"
11 | )
12 |
13 | var (
14 | releaseCreateCommit, releaseCreateRelease, releaseCreateReleaseDate string
15 | releaseCreateCreatorEmail, releaseCreateCreatorName, releaseCreateMsg string
16 | )
17 |
18 | // Creates a release for a database
19 | var releaseCreateCmd = &cobra.Command{
20 | Use: "create [database name] --release xxx --commit yyy",
21 | Short: "Create a release for a database",
22 | RunE: func(cmd *cobra.Command, args []string) error {
23 | return releaseCreate(args)
24 | },
25 | }
26 |
27 | func init() {
28 | releaseCmd.AddCommand(releaseCreateCmd)
29 | releaseCreateCmd.Flags().StringVar(&releaseCreateCommit, "commit", "", "Commit ID for the new release")
30 | releaseCreateCmd.Flags().StringVar(&releaseCreateCreatorEmail, "email", "", "Email address of release creator")
31 | releaseCreateCmd.Flags().StringVar(&releaseCreateCreatorName, "name", "", "Name of release creator")
32 | releaseCreateCmd.Flags().StringVar(&releaseCreateMsg, "message", "", "Description / message for the release")
33 | releaseCreateCmd.Flags().StringVar(&releaseCreateRelease, "release", "", "Name of release to create")
34 | releaseCreateCmd.Flags().StringVar(&releaseCreateReleaseDate, "date", "", "Custom timestamp (RFC3339 format) for release")
35 | }
36 |
37 | func releaseCreate(args []string) error {
38 | // Ensure a database file was given
39 | var db string
40 | var err error
41 | var meta metaData
42 | if len(args) == 0 {
43 | db, err = getDefaultDatabase()
44 | if err != nil {
45 | return err
46 | }
47 | if db == "" {
48 | // No database name was given on the command line, and we don't have a default database selected
49 | return errors.New("No database file specified")
50 | }
51 | } else {
52 | db = args[0]
53 | }
54 | if len(args) > 1 {
55 | return errors.New("Only one database can be changed at a time (for now)")
56 | }
57 |
58 | // Ensure a new release name and commit ID were given
59 | if releaseCreateRelease == "" {
60 | return errors.New("No release name given")
61 | }
62 | if releaseCreateCommit == "" {
63 | return errors.New("No commit ID given")
64 | }
65 |
66 | // Make sure we have the email and name of the release creator. Either by loading it from the config file, or
67 | // getting it from the command line arguments
68 | if releaseCreateCreatorEmail == "" {
69 | if viper.IsSet("user.email") == false {
70 | return errors.New("No email address provided")
71 | }
72 | releaseCreateCreatorEmail = viper.GetString("user.email")
73 | }
74 |
75 | if releaseCreateCreatorName == "" {
76 | if viper.IsSet("user.name") == false {
77 | return errors.New("No name provided")
78 | }
79 | releaseCreateCreatorName = viper.GetString("user.name")
80 | }
81 |
82 | // Make sure the database file exists, and get it's file size
83 | fileInfo, err := os.Stat(db)
84 | if os.IsNotExist(err) {
85 | return err
86 | }
87 | size := fileInfo.Size()
88 |
89 | // If a date was given, parse it to ensure the format is correct. Warn the user if it isn't,
90 | releaseTimeStamp := time.Now()
91 | if releaseCreateReleaseDate != "" {
92 | releaseTimeStamp, err = time.Parse(time.RFC3339, releaseCreateReleaseDate)
93 | if err != nil {
94 | return err
95 | }
96 | }
97 |
98 | // Load the metadata
99 | meta, err = loadMetadata(db)
100 | if err != nil {
101 | return err
102 | }
103 |
104 | // Ensure a release with the same name doesn't already exist
105 | if _, ok := meta.Releases[releaseCreateRelease]; ok == true {
106 | return errors.New("A release with that name already exists")
107 | }
108 |
109 | // Generate the new release info locally
110 | newRelease := releaseEntry{
111 | Commit: releaseCreateCommit,
112 | Date: releaseTimeStamp,
113 | Description: releaseCreateMsg,
114 | ReleaserEmail: releaseCreateCreatorEmail,
115 | ReleaserName: releaseCreateCreatorName,
116 | Size: size,
117 | }
118 |
119 | // Add the new release to the local metadata cache
120 | meta.Releases[releaseCreateRelease] = newRelease
121 |
122 | // Save the updated metadata back to disk
123 | err = saveMetadata(db, meta)
124 | if err != nil {
125 | return err
126 | }
127 |
128 | _, err = fmt.Fprintln(fOut, "Release creation succeeded")
129 | return err
130 | }
131 |
--------------------------------------------------------------------------------
/cmd/releaseList.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "sort"
7 | "time"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // Displays the list of releases for a remote database
13 | var releaseListCmd = &cobra.Command{
14 | Use: "releases [database name]",
15 | Short: "Displays a list of releases for a database",
16 | RunE: func(cmd *cobra.Command, args []string) error {
17 | return releaseList(args)
18 | },
19 | }
20 |
21 | func init() {
22 | RootCmd.AddCommand(releaseListCmd)
23 | }
24 |
25 | func releaseList(args []string) error {
26 | // Ensure a database file was given
27 | var db string
28 | var err error
29 | var meta metaData
30 | if len(args) == 0 {
31 | db, err = getDefaultDatabase()
32 | if err != nil {
33 | return err
34 | }
35 | if db == "" {
36 | // No database name was given on the command line, and we don't have a default database selected
37 | return errors.New("No database file specified")
38 | }
39 | } else {
40 | db = args[0]
41 | }
42 | if len(args) > 1 {
43 | return errors.New("Only one database can be worked with at a time (for now)")
44 | }
45 |
46 | // If there is a local metadata cache for the requested database, use that. Otherwise, retrieve it from the
47 | // server first (without storing it)
48 | meta, err = localFetchMetadata(db, true)
49 | if err != nil {
50 | return err
51 | }
52 |
53 | if len(meta.Releases) == 0 {
54 | _, err = fmt.Fprintf(fOut, "Database %s has no releases\n", db)
55 | return err
56 | }
57 |
58 | // Sort the list alphabetically
59 | var sortedKeys []string
60 | for k := range meta.Releases {
61 | sortedKeys = append(sortedKeys, k)
62 | }
63 | sort.Strings(sortedKeys)
64 |
65 | // Display the list of releases
66 | _, err = fmt.Fprintf(fOut, "Releases for %s:\n\n", db)
67 | if err != nil {
68 | return err
69 | }
70 | for _, i := range sortedKeys {
71 | _, err = fmt.Fprintf(fOut, " * '%s' : commit %s\n\n", i, meta.Releases[i].Commit)
72 | if err != nil {
73 | return err
74 | }
75 | _, err = fmt.Fprintf(fOut, " Author: %s <%s>\n", meta.Releases[i].ReleaserName, meta.Releases[i].ReleaserEmail)
76 | if err != nil {
77 | return err
78 | }
79 | _, err = fmt.Fprintf(fOut, " Date: %s\n", meta.Releases[i].Date.Format(time.UnixDate))
80 | if err != nil {
81 | return err
82 | }
83 | _, err = fmt.Fprintln(fOut, numFormat.Sprintf(" Size: %d", meta.Releases[i].Size))
84 | if err != nil {
85 | return err
86 | }
87 | if meta.Releases[i].Description != "" {
88 | _, err = fmt.Fprintf(fOut, " Message: %s\n\n", meta.Releases[i].Description)
89 | if err != nil {
90 | return err
91 | }
92 | } else {
93 | _, err = fmt.Fprintln(fOut)
94 | if err != nil {
95 | return err
96 | }
97 | }
98 | }
99 | return nil
100 | }
101 |
--------------------------------------------------------------------------------
/cmd/releaseRemove.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var releaseRemoveRelease string
11 |
12 | // Removes a release from a database
13 | var releaseRemoveCmd = &cobra.Command{
14 | Use: "remove [database name] --release xxx",
15 | Short: "Remove a release from a database",
16 | RunE: func(cmd *cobra.Command, args []string) error {
17 | return releaseRemove(args)
18 | },
19 | }
20 |
21 | func init() {
22 | releaseCmd.AddCommand(releaseRemoveCmd)
23 | releaseRemoveCmd.Flags().StringVar(&releaseRemoveRelease, "release", "", "Name of release to remove")
24 | }
25 |
26 | func releaseRemove(args []string) error {
27 | // Ensure a database file was given
28 | var db string
29 | var err error
30 | var meta metaData
31 | if len(args) == 0 {
32 | db, err = getDefaultDatabase()
33 | if err != nil {
34 | return err
35 | }
36 | if db == "" {
37 | // No database name was given on the command line, and we don't have a default database selected
38 | return errors.New("No database file specified")
39 | }
40 | } else {
41 | db = args[0]
42 | }
43 | if len(args) > 1 {
44 | return errors.New("Only one database can be changed at a time (for now)")
45 | }
46 |
47 | // Ensure a release name was given
48 | if releaseRemoveRelease == "" {
49 | return errors.New("No release name given")
50 | }
51 |
52 | // Load the metadata
53 | meta, err = loadMetadata(db)
54 | if err != nil {
55 | return err
56 | }
57 |
58 | // Check if the release exists
59 | if _, ok := meta.Releases[releaseRemoveRelease]; ok != true {
60 | return errors.New("A release with that name doesn't exist")
61 | }
62 |
63 | // Remove the release
64 | delete(meta.Releases, releaseRemoveRelease)
65 |
66 | // Save the updated metadata back to disk
67 | err = saveMetadata(db, meta)
68 | if err != nil {
69 | return err
70 | }
71 |
72 | _, err = fmt.Fprintf(fOut, "Release '%s' removed\n", releaseRemoveRelease)
73 | return err
74 | }
75 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "log"
10 | "os"
11 | "path/filepath"
12 |
13 | "github.com/mitchellh/go-homedir"
14 | "github.com/spf13/cobra"
15 | "github.com/spf13/viper"
16 | "golang.org/x/text/message"
17 | )
18 |
19 | const (
20 | DIO_VERSION = "0.3.1"
21 | )
22 |
23 | var (
24 | certUser string
25 | cfgFile, cloud string
26 | fOut = io.Writer(os.Stdout)
27 | numFormat *message.Printer
28 | TLSConfig tls.Config
29 | )
30 |
31 | // RootCmd represents the base command when called without any subcommands
32 | var RootCmd = &cobra.Command{
33 | Use: "dio",
34 | Short: "Command line interface to DBHub.io",
35 | Long: `dio is a command line interface (CLI) for DBHub.io.
36 |
37 | With dio you can send and receive database files to a DBHub.io cloud,
38 | and manipulate its tags and branches.`,
39 | SilenceErrors: true,
40 | SilenceUsage: true,
41 | }
42 |
43 | // Execute adds all child commands to the root command & sets flags appropriately.
44 | // This is called by main.main(). It only needs to happen once to the rootCmd.
45 | func Execute() {
46 | if err := RootCmd.Execute(); err != nil {
47 | fmt.Println(err)
48 | os.Exit(1)
49 | }
50 | }
51 |
52 | func init() {
53 | // Add support for pretty printing numbers
54 | numFormat = message.NewPrinter(message.MatchLanguage("en"))
55 |
56 | // When run from go test we skip this, as we generate a temporary config file in the test suite setup
57 | if os.Getenv("IS_TESTING") == "yes" {
58 | return
59 | }
60 |
61 | // Add the global environment variables
62 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
63 | fmt.Sprintf("config file (default is %s)", filepath.Join("$HOME", ".dio", "config.toml")))
64 | RootCmd.PersistentFlags().StringVar(&cloud, "cloud", "https://db4s.dbhub.io",
65 | "Address of the DBHub.io cloud")
66 |
67 | // Read all of our configuration data now
68 | if cfgFile != "" {
69 | // Use config file from the flag
70 | viper.SetConfigFile(cfgFile)
71 | } else {
72 | // Find home directory
73 | home, err := homedir.Dir()
74 | if err != nil {
75 | fmt.Println(err)
76 | os.Exit(1)
77 | }
78 |
79 | // Search for config in ".dio" subdirectory under the users home directory
80 | p := filepath.Join(home, ".dio")
81 | viper.AddConfigPath(p)
82 | viper.SetConfigName("config")
83 | cfgFile = filepath.Join(p, "config.toml")
84 | }
85 |
86 | // If a config file is found, read it in.
87 | if err := viper.ReadInConfig(); err != nil {
88 | // No configuration file was found, so generate a default one and let the user know they need to supply the
89 | // missing info
90 | errInner := generateConfig(cfgFile)
91 | if errInner != nil {
92 | log.Fatalln(errInner)
93 | return
94 | }
95 | log.Fatalf("No usable configuration file was found, so a default one has been generated in: %s\n"+
96 | "Please update it with your name, and the path to your DBHub.io user certificate file.\n", cfgFile)
97 | return
98 | }
99 |
100 | // Make sure the paths to our CA Chain and user certificate have been set
101 | if found := viper.IsSet("certs.cachain"); found == false {
102 | log.Fatal("Path to Certificate Authority chain file not set in the config file")
103 | return
104 | }
105 | if found := viper.IsSet("certs.cert"); found == false {
106 | log.Fatal("Path to user certificate file not set in the config file")
107 | return
108 | }
109 |
110 | // If an alternative DBHub.io cloud address is set in the config file, use that
111 | if found := viper.IsSet("general.cloud"); found == true {
112 | // If the user provided an override on the command line, that will override this anyway
113 | cloud = viper.GetString("general.cloud")
114 | }
115 |
116 | // Read our certificate info, if present
117 | ourCAPool := x509.NewCertPool()
118 | chainFile, err := ioutil.ReadFile(viper.GetString("certs.cachain"))
119 | if err != nil {
120 | log.Fatal(err)
121 | }
122 | ok := ourCAPool.AppendCertsFromPEM(chainFile)
123 | if !ok {
124 | log.Fatal("Error when loading certificate chain file")
125 | }
126 |
127 | // TODO: Check if the client certificate file is present
128 | certFile := viper.GetString("certs.cert")
129 | if _, err = os.Stat(certFile); err != nil {
130 | log.Fatalf("Please download your client certificate from DBHub.io, then update the configuration "+
131 | "file '%s' with its path", cfgFile)
132 | }
133 |
134 | // Load a client certificate file
135 | cert, err := tls.LoadX509KeyPair(certFile, certFile)
136 | if err != nil {
137 | log.Fatal(err)
138 | }
139 |
140 | // Load our self signed CA Cert chain, and set TLS1.2 as minimum
141 | TLSConfig = tls.Config{
142 | Certificates: []tls.Certificate{cert},
143 | ClientCAs: ourCAPool,
144 | InsecureSkipVerify: true,
145 | MinVersion: tls.VersionTLS12,
146 | PreferServerCipherSuites: true,
147 | RootCAs: ourCAPool,
148 | }
149 |
150 | // Extract the username and email from the TLS certificate
151 | var email string
152 | certUser, email, _, err = getUserAndServer()
153 | if err != nil {
154 | log.Fatal(err)
155 | }
156 | viper.Set("user.email", email)
157 | }
158 |
--------------------------------------------------------------------------------
/cmd/select.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | // Selects the default database, or if no database name is given it displays the default database
11 | var selectCmd = &cobra.Command{
12 | Use: "select",
13 | Short: "Selects the default database used by all dio commands",
14 | RunE: func(cmd *cobra.Command, args []string) error {
15 | return selectDefault(args)
16 | },
17 | }
18 |
19 | func init() {
20 | RootCmd.AddCommand(selectCmd)
21 | }
22 |
23 | func selectDefault(args []string) error {
24 | // Ensure a database file was given
25 | var db string
26 | var err error
27 | if len(args) == 0 {
28 | db, err = getDefaultDatabase()
29 | if err != nil {
30 | return err
31 | }
32 | _, err = fmt.Fprintf(fOut, "Default database: '%s'\n", db)
33 | if err != nil {
34 | return err
35 | }
36 | return nil
37 | }
38 | if len(args) > 1 {
39 | return errors.New("Only one database can be selected as the default (for now)")
40 | }
41 |
42 | // Save the given text string as the default database
43 | // TODO: Add some error checking here (eg does the database exist locally or remotely?)
44 | db = args[0]
45 | err = saveDefaultDatabase(db)
46 | if err != nil {
47 | return err
48 | }
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/cmd/shared.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha256"
6 | "crypto/tls"
7 | "crypto/x509"
8 | "encoding/hex"
9 | "encoding/json"
10 | "errors"
11 | "fmt"
12 | "io/ioutil"
13 | "log"
14 | "net/http"
15 | "net/url"
16 | "os"
17 | "path/filepath"
18 | "runtime"
19 | "strings"
20 | "time"
21 |
22 | "github.com/mitchellh/go-homedir"
23 | rq "github.com/parnurzeal/gorequest"
24 | )
25 |
26 | // Check if the database with the given SHA256 checksum is in local cache. If it's not then download and cache it
27 | func checkDBCache(db, shaSum string) (err error) {
28 | if _, err = os.Stat(filepath.Join(".dio", db, "db", shaSum)); os.IsNotExist(err) {
29 | var body []byte
30 | _, body, err = retrieveDatabase(db, pullCmdBranch, pullCmdCommit)
31 | if err != nil {
32 | return
33 | }
34 |
35 | // Verify the SHA256 checksum of the new download
36 | s := sha256.Sum256(body)
37 | thisSum := hex.EncodeToString(s[:])
38 | if thisSum != shaSum {
39 | // The newly downloaded database file doesn't have the expected checksum. Abort.
40 | return errors.New(fmt.Sprintf("Aborting: newly downloaded database file should have "+
41 | "checksum '%s', but data with checksum '%s' received\n", shaSum, thisSum))
42 | }
43 |
44 | // Write the database file to disk in the cache directory
45 | err = ioutil.WriteFile(filepath.Join(".dio", db, "db", shaSum), body, 0644)
46 | }
47 | return
48 | }
49 |
50 | // Generate a stable SHA256 for a commit.
51 | func createCommitID(c commitEntry) string {
52 | var b bytes.Buffer
53 | b.WriteString(fmt.Sprintf("tree %s\n", c.Tree.ID))
54 | if c.Parent != "" {
55 | b.WriteString(fmt.Sprintf("parent %s\n", c.Parent))
56 | }
57 | for _, j := range c.OtherParents {
58 | b.WriteString(fmt.Sprintf("parent %s\n", j))
59 | }
60 | b.WriteString(fmt.Sprintf("author %s <%s> %v\n", c.AuthorName, c.AuthorEmail,
61 | c.Timestamp.UTC().Format(time.UnixDate)))
62 | if c.CommitterEmail != "" {
63 | b.WriteString(fmt.Sprintf("committer %s <%s> %v\n", c.CommitterName, c.CommitterEmail,
64 | c.Timestamp.UTC().Format(time.UnixDate)))
65 | }
66 | b.WriteString("\n" + c.Message)
67 | b.WriteByte(0)
68 | s := sha256.Sum256(b.Bytes())
69 | return hex.EncodeToString(s[:])
70 | }
71 |
72 | // Generate the SHA256 for a tree.
73 | // Tree entry structure is:
74 | // * [ entry type ] [ licence sha256] [ file sha256 ] [ file name ] [ last modified (timestamp) ] [ file size (bytes) ]
75 | func createDBTreeID(entries []dbTreeEntry) string {
76 | var b bytes.Buffer
77 | for _, j := range entries {
78 | b.WriteString(string(j.EntryType))
79 | b.WriteByte(0)
80 | b.WriteString(string(j.LicenceSHA))
81 | b.WriteByte(0)
82 | b.WriteString(j.Sha256)
83 | b.WriteByte(0)
84 | b.WriteString(j.Name)
85 | b.WriteByte(0)
86 | b.WriteString(j.LastModified.Format(time.RFC3339))
87 | b.WriteByte(0)
88 | b.WriteString(fmt.Sprintf("%d\n", j.Size))
89 | }
90 | s := sha256.Sum256(b.Bytes())
91 | return hex.EncodeToString(s[:])
92 | }
93 |
94 | // Returns true if a database has been changed on disk since the last commit
95 | func dbChanged(db string, meta metaData) (changed bool, err error) {
96 | // Retrieve the sha256, file size, and last modified date from the head commit of the active branch
97 | head, ok := meta.Branches[meta.ActiveBranch]
98 | if !ok {
99 | err = errors.New("Aborting: info for the active branch isn't found in the local branch cache")
100 | return
101 | }
102 | c, ok := meta.Commits[head.Commit]
103 | if !ok {
104 | err = errors.New("Aborting: info for the head commit isn't found in the local commit cache")
105 | return
106 | }
107 | metaSHASum := c.Tree.Entries[0].Sha256
108 | metaFileSize := c.Tree.Entries[0].Size
109 | metaLastModified := c.Tree.Entries[0].LastModified.Truncate(time.Second).UTC()
110 |
111 | // If the file size or last modified date in the metadata are different from the current file info, then the
112 | // local file has probably changed. Well, "probably" for the last modified day, but "definitely" if the file
113 | // size is different
114 | fi, err := os.Stat(db)
115 | if err != nil {
116 | if os.IsNotExist(err) {
117 | return false, nil
118 | }
119 | return
120 | }
121 | fileSize := fi.Size()
122 | lastModified := fi.ModTime().Truncate(time.Second).UTC()
123 | if metaFileSize != fileSize || !metaLastModified.Equal(lastModified) {
124 | changed = true
125 | return
126 | }
127 |
128 | // * If the file size and last modified date are still the same, we SHA256 checksum and compare the file *
129 |
130 | // TODO: Should we only do this for smaller files (below some TBD threshold)?
131 |
132 | // Read the database from disk, and calculate it's sha256
133 | b, err := ioutil.ReadFile(db)
134 | if err != nil {
135 | return
136 | }
137 | if int64(len(b)) != fileSize {
138 | err = errors.New(numFormat.Sprintf("Aborting: # of bytes read (%d) when reading the database "+
139 | "doesn't match the database file size (%d)", len(b), fileSize))
140 | return
141 | }
142 | s := sha256.Sum256(b)
143 | shaSum := hex.EncodeToString(s[:])
144 |
145 | // Check if a change has been made
146 | if metaSHASum != shaSum {
147 | changed = true
148 | }
149 | return
150 | }
151 |
152 | // Retrieves the list of databases available to the user
153 | var getDatabases = func(url string, user string) (dbList []dbListEntry, err error) {
154 | resp, body, errs := rq.New().TLSClientConfig(&TLSConfig).
155 | Get(fmt.Sprintf("%s/%s", url, user)).
156 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)).
157 | EndBytes()
158 | if errs != nil {
159 | e := fmt.Sprintln("Errors when retrieving the database list:")
160 | for _, err := range errs {
161 | e += fmt.Sprintf(err.Error())
162 | }
163 | err = errors.New(e)
164 | return
165 | }
166 | defer resp.Body.Close()
167 | err = json.Unmarshal(body, &dbList)
168 | if err != nil {
169 | _, errInner := fmt.Fprintf(fOut, "Error retrieving database list: '%v'\n", err.Error())
170 | if errInner != nil {
171 | err = fmt.Errorf("%s: %s", err, errInner)
172 | return
173 | }
174 | }
175 | return
176 | }
177 |
178 | // Generates an initial default (production) configuration file. Before it's useful, the user will need to fill out
179 | // their display name + provide a DB4S certificate file
180 | func generateConfig(cfgFile string) (err error) {
181 | // Create the ".dio" directory in the users home folder, to store the configuration file in
182 | var home string
183 | home, err = homedir.Dir()
184 | if err != nil {
185 | return
186 | }
187 | if _, err = os.Stat(filepath.Join(home, ".dio")); os.IsNotExist(err) {
188 | err = os.Mkdir(filepath.Join(home, ".dio"), 0770)
189 | if err != nil {
190 | return
191 | }
192 | }
193 |
194 | // Download the Certificate Authority chain file
195 | caURL := "https://github.com/sqlitebrowser/dio/raw/master/cert/ca-chain.cert.pem"
196 | chainFile := filepath.Join(home, ".dio", "ca-chain.cert.pem")
197 | resp, body, errs := rq.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: true}).Get(caURL).
198 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)).
199 | EndBytes()
200 | if errs != nil {
201 | e := fmt.Sprintln("errors when retrieving the CA chain file:")
202 | for _, errInner := range errs {
203 | e += fmt.Sprintf(errInner.Error())
204 | }
205 | return errors.New(e)
206 | }
207 | defer resp.Body.Close()
208 | err = ioutil.WriteFile(chainFile, body, 0644)
209 | if err != nil {
210 | return err
211 | }
212 |
213 | // Generate the initial config file
214 | var f *os.File
215 | f, err = os.Create(cfgFile)
216 | if err != nil {
217 | return
218 | }
219 | defer f.Close()
220 | lineEnd := "\n"
221 | if runtime.GOOS == "windows" {
222 | lineEnd = "\r\n"
223 | }
224 | certPath := fmt.Sprintf("%c%s", os.PathSeparator, filepath.Join("path", "to", "your", "certificate", "here"))
225 | _, err = fmt.Fprint(f, `[certs]`+lineEnd)
226 | _, err = fmt.Fprint(f, fmt.Sprintf(`cachain = '%s'%s`, chainFile, lineEnd))
227 | _, err = fmt.Fprint(f, fmt.Sprintf(`cert = '%s'%s`, certPath, lineEnd))
228 | _, err = fmt.Fprint(f, lineEnd)
229 | _, err = fmt.Fprint(f, `[general]`+lineEnd)
230 | _, err = fmt.Fprint(f, `cloud = 'https://db4s.dbhub.io'`+lineEnd)
231 | _, err = fmt.Fprint(f, lineEnd)
232 | _, err = fmt.Fprint(f, `[user]`+lineEnd)
233 | _, err = fmt.Fprint(f, `name = 'Your Name'`+lineEnd)
234 | return
235 | }
236 |
237 | // Returns the name of the default database, if one has been selected. Returns an empty string if not
238 | func getDefaultDatabase() (db string, err error) {
239 | // Check if the local defaults info exists
240 | var z []byte
241 | if z, err = ioutil.ReadFile(filepath.Join(".dio", "defaults.json")); err != nil {
242 | if os.IsNotExist(err) {
243 | return "", nil
244 | }
245 | return
246 | }
247 |
248 | // Read and parse the metadata
249 | var y defaultSettings
250 | err = json.Unmarshal([]byte(z), &y)
251 | if err != nil {
252 | return
253 | }
254 | if y.SelectedDatabase != "" {
255 | db = y.SelectedDatabase
256 | }
257 | return
258 | }
259 |
260 | // Returns a map with the list of licences available on the remote server
261 | var getLicences = func() (list map[string]licenceEntry, err error) {
262 | // Retrieve the database list from the cloud
263 | resp, body, errs := rq.New().TLSClientConfig(&TLSConfig).Get(cloud+"/licence/list").
264 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)).
265 | End()
266 | if errs != nil {
267 | e := fmt.Sprintln("errors when retrieving the licence list:")
268 | for _, err := range errs {
269 | e += fmt.Sprintf(err.Error())
270 | }
271 | return list, errors.New(e)
272 | }
273 | defer resp.Body.Close()
274 |
275 | // Convert the JSON response to our licence entry structure
276 | err = json.Unmarshal([]byte(body), &list)
277 | if err != nil {
278 | return list, errors.New(fmt.Sprintf("error retrieving licence list: '%v'\n", err.Error()))
279 | }
280 | return list, err
281 | }
282 |
283 | // getUserAndServer() returns the user name and server from a DBHub.io client certificate
284 | func getUserAndServer() (userAcc string, email string, certServer string, err error) {
285 | if numCerts := len(TLSConfig.Certificates); numCerts == 0 {
286 | err = errors.New("No client certificates installed. Can't proceed.")
287 | return
288 | }
289 |
290 | // Parse the client certificate
291 | // TODO: Add support for multiple certificates
292 | cert, err := x509.ParseCertificate(TLSConfig.Certificates[0].Certificate[0])
293 | if err != nil {
294 | err = errors.New("Couldn't parse cert")
295 | return
296 | }
297 |
298 | // Extract the account name, email address, and associated server from the certificate
299 | email = cert.Subject.CommonName
300 | if email == "" {
301 | // The common name field is empty in the client cert. Can't proceed.
302 | err = errors.New("Common name is blank in client certificate")
303 | return
304 | }
305 | s := strings.Split(email, "@")
306 | if len(s) < 2 {
307 | err = errors.New("Missing information in client certificate")
308 | return
309 | }
310 | userAcc = s[0]
311 | certServer = s[1]
312 | if userAcc == "" || certServer == "" {
313 | // Missing details in common name field
314 | err = errors.New("Missing information in client certificate")
315 | return
316 | }
317 | return
318 | }
319 |
320 | // Loads the local metadata from disk (if present). If not, then grab it from the remote server, storing it locally.
321 | // Note - This is subtly different than calling updateMetadata() itself. This function
322 | // (loadMetadata()) is for use by commands which can use a local metadata cache all by itself
323 | // (eg branch creation), but only if it already exists. For those, it only calls the
324 | // remote server when a local metadata cache doesn't exist.
325 | func loadMetadata(db string) (meta metaData, err error) {
326 | // Check if the local metadata exists. If not, pull it from the remote server
327 | if _, err = os.Stat(filepath.Join(".dio", db, "metadata.json")); os.IsNotExist(err) {
328 | _, err = updateMetadata(db, true)
329 | if err != nil {
330 | return
331 | }
332 | }
333 |
334 | // Read and parse the metadata
335 | var md []byte
336 | md, err = ioutil.ReadFile(filepath.Join(".dio", db, "metadata.json"))
337 | if err != nil {
338 | return
339 | }
340 | err = json.Unmarshal([]byte(md), &meta)
341 |
342 | // If the tag or release maps are missing, create initial empty ones.
343 | // This is a safety check, not sure if it's really needed
344 | if meta.Tags == nil {
345 | meta.Tags = make(map[string]tagEntry)
346 | }
347 | if meta.Releases == nil {
348 | meta.Releases = make(map[string]releaseEntry)
349 | }
350 | return
351 | }
352 |
353 | // Loads the local metadata cache for the requested database, if present. Otherwise, (optionally) retrieve it from
354 | // the server.
355 | // Note - this is suitable for use by read-only functions (eg: branch/tag list, log)
356 | // as it doesn't store or change any metadata on disk
357 | var localFetchMetadata = func(db string, getRemote bool) (meta metaData, err error) {
358 | md, err := ioutil.ReadFile(filepath.Join(".dio", db, "metadata.json"))
359 | if err == nil {
360 | err = json.Unmarshal([]byte(md), &meta)
361 | return
362 | }
363 |
364 | // Can't read local metadata, and we're requested to not grab remote metadata. So, nothing to do but exit
365 | if !getRemote {
366 | err = errors.New("No local metadata for the database exists")
367 | return
368 | }
369 |
370 | // Can't read local metadata, but we're ok to grab the remote. So, use that instead
371 | meta, _, err = retrieveMetadata(db)
372 | return
373 | }
374 |
375 | // Merges old and new metadata
376 | func mergeMetadata(origMeta metaData, newMeta metaData) (mergedMeta metaData, err error) {
377 | mergedMeta.Branches = make(map[string]branchEntry)
378 | mergedMeta.Commits = make(map[string]commitEntry)
379 | mergedMeta.Tags = make(map[string]tagEntry)
380 | mergedMeta.Releases = make(map[string]releaseEntry)
381 | if len(origMeta.Commits) > 0 {
382 | // Start by check branches which exist locally
383 | // TODO: Change sort order to be by alphabetical branch name, as the current unordered approach leads to
384 | // inconsistent output across runs
385 | for brName, brData := range origMeta.Branches {
386 | matchFound := false
387 | for newBranch, newData := range newMeta.Branches {
388 | if brName == newBranch {
389 | // A branch with this name exists on both the local and remote server
390 | matchFound = true
391 | skipFurtherChecks := false
392 |
393 | // Rewind back to the local root commit, making a list of the local commits IDs we pass through
394 | var localList []string
395 | localCommit := origMeta.Commits[brData.Commit]
396 | localList = append(localList, localCommit.ID)
397 | for localCommit.Parent != "" {
398 | localCommit = origMeta.Commits[localCommit.Parent]
399 | localList = append(localList, localCommit.ID)
400 | }
401 | localLength := len(localList) - 1
402 |
403 | // Rewind back to the remote root commit, making a list of the remote commit IDs we pass through
404 | var remoteList []string
405 | remoteCommit := newMeta.Commits[newData.Commit]
406 | remoteList = append(remoteList, remoteCommit.ID)
407 | for remoteCommit.Parent != "" {
408 | remoteCommit = newMeta.Commits[remoteCommit.Parent]
409 | remoteList = append(remoteList, remoteCommit.ID)
410 | }
411 | remoteLength := len(remoteList) - 1
412 |
413 | // Make sure the local and remote commits start out with the same commit ID
414 | if localCommit.ID != remoteCommit.ID {
415 | // The local and remote branches don't have a common root, so abort
416 | err = errors.New(fmt.Sprintf("Local and remote branch %s don't have a common root. "+
417 | "Aborting.", brName))
418 | return
419 | }
420 |
421 | // If there are more commits in the local branch than in the remote one, we keep the local branch
422 | // as it probably means the user is adding stuff locally (prior to pushing to the server)
423 | if localLength > remoteLength {
424 | c := origMeta.Commits[brData.Commit]
425 | mergedMeta.Commits[c.ID] = origMeta.Commits[c.ID]
426 | for c.Parent != "" {
427 | c = origMeta.Commits[c.Parent]
428 | mergedMeta.Commits[c.ID] = origMeta.Commits[c.ID]
429 | }
430 |
431 | // Copy the local branch data
432 | mergedMeta.Branches[brName] = brData
433 | }
434 |
435 | // We've wound back to the root commit for both the local and remote branch, and the root commit
436 | // IDs match. Now we walk forwards through the commits, comparing them.
437 | branchesSame := true
438 | for i := 0; i <= localLength; i++ {
439 | lCommit := localList[localLength-i]
440 | if i > remoteLength {
441 | branchesSame = false
442 | } else {
443 | if lCommit != remoteList[remoteLength-i] {
444 | // There are conflicting commits in this branch between the local metadata and the
445 | // remote. This will probably need to be resolved by user action.
446 | branchesSame = false
447 | }
448 | }
449 | }
450 |
451 | // If the local branch commits are in the remote branch already, then we only need to check for
452 | // newer commits in the remote branch
453 | if branchesSame {
454 | if remoteLength > localLength {
455 | _, err = fmt.Fprintf(fOut, " * Remote branch '%s' has %d new commit(s)... merged\n",
456 | brName, remoteLength-localLength)
457 | if err != nil {
458 | return
459 | }
460 | for _, j := range remoteList {
461 | mergedMeta.Commits[j] = newMeta.Commits[j]
462 | }
463 | mergedMeta.Branches[brName] = newMeta.Branches[brName]
464 | } else {
465 | // The local and remote branches are the same, so copy the local branch commits across to
466 | // the merged data structure
467 | _, err = fmt.Fprintf(fOut, " * Branch '%s' is unchanged\n", brName)
468 | if err != nil {
469 | return
470 | }
471 | for _, j := range localList {
472 | mergedMeta.Commits[j] = origMeta.Commits[j]
473 | }
474 | mergedMeta.Branches[brName] = brData
475 | }
476 | // No need to do further checks on this branch
477 | skipFurtherChecks = true
478 | }
479 |
480 | if skipFurtherChecks == false && brData.Commit != newData.Commit {
481 | _, err = fmt.Fprintf(fOut, " * Branch '%s' has local changes, not on the server\n",
482 | brName)
483 | if err != nil {
484 | return
485 | }
486 |
487 | // Copy across the commits from the local branch
488 | localCommit := origMeta.Commits[brData.Commit]
489 | mergedMeta.Commits[localCommit.ID] = origMeta.Commits[localCommit.ID]
490 | for localCommit.Parent != "" {
491 | localCommit = origMeta.Commits[localCommit.Parent]
492 | mergedMeta.Commits[localCommit.ID] = origMeta.Commits[localCommit.ID]
493 | }
494 |
495 | // Copy across the branch data entry for the local branch
496 | mergedMeta.Branches[brName] = brData
497 | }
498 | if skipFurtherChecks == false && brData.Description != newData.Description {
499 | _, err = fmt.Fprintf(fOut, " * Description for branch %s differs between the local "+
500 | "and remote\n"+
501 | " * Local: '%s'\n"+
502 | " * Remote: '%s'\n", brName, brData.Description, newData.Description)
503 | if err != nil {
504 | return
505 | }
506 | }
507 | }
508 | }
509 | if !matchFound {
510 | // This seems to be a branch that's not on the server, so we keep it as-is
511 | _, err = fmt.Fprintf(fOut, " * Branch '%s' is local only, not on the server\n", brName)
512 | if err != nil {
513 | return
514 | }
515 | mergedMeta.Branches[brName] = brData
516 |
517 | // Copy across the commits from the local branch
518 | localCommit := origMeta.Commits[brData.Commit]
519 | mergedMeta.Commits[localCommit.ID] = origMeta.Commits[localCommit.ID]
520 | for localCommit.Parent != "" {
521 | localCommit = origMeta.Commits[localCommit.Parent]
522 | mergedMeta.Commits[localCommit.ID] = origMeta.Commits[localCommit.ID]
523 | }
524 |
525 | // Copy across the branch data entry for the local branch
526 | mergedMeta.Branches[brName] = brData
527 | }
528 | }
529 |
530 | // Add new branches
531 | for remoteName, remoteData := range newMeta.Branches {
532 | if _, ok := origMeta.Branches[remoteName]; ok == false {
533 | // Copy their commit data
534 | newCommit := newMeta.Commits[remoteData.Commit]
535 | mergedMeta.Commits[newCommit.ID] = newMeta.Commits[newCommit.ID]
536 | for newCommit.Parent != "" {
537 | newCommit = newMeta.Commits[newCommit.Parent]
538 | mergedMeta.Commits[newCommit.ID] = newMeta.Commits[newCommit.ID]
539 | }
540 |
541 | // Copy their branch data
542 | mergedMeta.Branches[remoteName] = remoteData
543 |
544 | _, err = fmt.Fprintf(fOut, " * New remote branch '%s' merged\n", remoteName)
545 | if err != nil {
546 | return
547 | }
548 | }
549 | }
550 |
551 | // Preserve existing tags
552 | for tagName, tagData := range origMeta.Tags {
553 | mergedMeta.Tags[tagName] = tagData
554 | }
555 |
556 | // Add new tags
557 | for tagName, tagData := range newMeta.Tags {
558 | // Only add tags which aren't already in the merged metadata structure
559 | if _, tagFound := mergedMeta.Tags[tagName]; tagFound == false {
560 | // Also make sure its commit is in the commit list. If it's not, then skip adding the tag
561 | if _, commitFound := mergedMeta.Commits[tagData.Commit]; commitFound == true {
562 | _, err = fmt.Fprintf(fOut, " * New tag '%s' merged\n", tagName)
563 | if err != nil {
564 | return
565 | }
566 | mergedMeta.Tags[tagName] = tagData
567 | }
568 | }
569 | }
570 |
571 | // Preserve existing releases
572 | for relName, relData := range origMeta.Releases {
573 | mergedMeta.Releases[relName] = relData
574 | }
575 |
576 | // Add new releases
577 | for relName, relData := range newMeta.Releases {
578 | // Only add releases which aren't already in the merged metadata structure
579 | if _, relFound := mergedMeta.Releases[relName]; relFound == false {
580 | // Also make sure its commit is in the commit list. If it's not, then skip adding the release
581 | if _, commitFound := mergedMeta.Commits[relData.Commit]; commitFound == true {
582 | _, err = fmt.Fprintf(fOut, " * New release '%s' merged\n", relName)
583 | if err != nil {
584 | return
585 | }
586 | mergedMeta.Releases[relName] = relData
587 | }
588 | }
589 | }
590 |
591 | // Copy the default branch name from the remote server
592 | mergedMeta.DefBranch = newMeta.DefBranch
593 |
594 | // If an active (local) branch has been set, then copy it to the merged metadata. Otherwise use the default
595 | // branch as given by the remote server
596 | if origMeta.ActiveBranch != "" {
597 | mergedMeta.ActiveBranch = origMeta.ActiveBranch
598 | } else {
599 | mergedMeta.ActiveBranch = newMeta.DefBranch
600 | }
601 |
602 | _, err = fmt.Fprintln(fOut)
603 | if err != nil {
604 | return
605 | }
606 | } else {
607 | // No existing metadata, so just copy across the remote metadata
608 | mergedMeta = newMeta
609 |
610 | // Use the remote default branch as the initial active (local) branch
611 | mergedMeta.ActiveBranch = newMeta.DefBranch
612 | }
613 | return
614 | }
615 |
616 | // Retrieves a database from DBHub.io
617 | func retrieveDatabase(db string, branch string, commit string) (resp rq.Response, body []byte, err error) {
618 | dbURL := fmt.Sprintf("%s/%s/%s", cloud, certUser, db)
619 | req := rq.New().TLSClientConfig(&TLSConfig).Get(dbURL).
620 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION))
621 | if branch != "" {
622 | req.Query(fmt.Sprintf("branch=%s", url.QueryEscape(branch)))
623 | } else {
624 | req.Query(fmt.Sprintf("commit=%s", url.QueryEscape(commit)))
625 | }
626 | var errs []error
627 | resp, body, errs = req.EndBytes()
628 | if errs != nil {
629 | log.Print("Errors when downloading database:")
630 | for _, err := range errs {
631 | log.Print(err.Error())
632 | }
633 | err = errors.New("Error when downloading database")
634 | return
635 | }
636 | if resp.StatusCode != http.StatusOK {
637 | if resp.StatusCode == http.StatusNotFound {
638 | if branch != "" {
639 | err = errors.New(fmt.Sprintf("That database & branch '%s' aren't known on DBHub.io",
640 | branch))
641 | return
642 | }
643 | if commit != "" {
644 | err = errors.New(fmt.Sprintf("Requested database not found with commit %s.",
645 | commit))
646 | return
647 | }
648 | err = errors.New("Requested database not found")
649 | return
650 | }
651 | err = errors.New(fmt.Sprintf("Download failed with an error: HTTP status %d - '%v'\n",
652 | resp.StatusCode, resp.Status))
653 | }
654 | return
655 | }
656 |
657 | // Retrieves database metadata from DBHub.io
658 | var retrieveMetadata = func(db string) (meta metaData, onCloud bool, err error) {
659 | // Download the database metadata
660 | resp, md, errs := rq.New().TLSClientConfig(&TLSConfig).Get(cloud+"/metadata/get").
661 | Query(fmt.Sprintf("username=%s", url.QueryEscape(certUser))).
662 | Query(fmt.Sprintf("folder=%s", "/")).
663 | Query(fmt.Sprintf("dbname=%s", url.QueryEscape(db))).
664 | Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)).
665 | End()
666 |
667 | if errs != nil {
668 | log.Print("Errors when downloading database metadata:")
669 | for _, err := range errs {
670 | log.Print(err.Error())
671 | }
672 | return metaData{}, false, errors.New("Error when downloading database metadata")
673 | }
674 | if resp.StatusCode == http.StatusNotFound {
675 | return metaData{}, false, nil
676 | }
677 | if resp.StatusCode != http.StatusOK {
678 | return metaData{}, false,
679 | errors.New(fmt.Sprintf("Metadata download failed with an error: HTTP status %d - '%v'\n",
680 | resp.StatusCode, resp.Status))
681 | }
682 | err = json.Unmarshal([]byte(md), &meta)
683 | if err != nil {
684 | return
685 | }
686 | return meta, true, nil
687 | }
688 |
689 | // Returns the name of the default database, if one has been selected. Returns an empty string if not
690 | func saveDefaultDatabase(db string) (err error) {
691 | // Load the local default info
692 | var z []byte
693 | var def defaultSettings
694 | if z, err = ioutil.ReadFile(filepath.Join(".dio", "defaults.json")); err == nil {
695 | err = json.Unmarshal([]byte(z), &def)
696 | if err != nil {
697 | return
698 | }
699 | } else {
700 | // No local default info, so we use a new blank set instead
701 | def = defaultSettings{}
702 | }
703 |
704 | // Save the new default database setting to disk
705 | def.SelectedDatabase = db
706 | var j []byte
707 | j, err = json.MarshalIndent(def, "", " ")
708 | if err != nil {
709 | return
710 | }
711 | err = ioutil.WriteFile(filepath.Join(".dio", "defaults.json"), j, 0644)
712 | return
713 | }
714 |
715 | // Saves the metadata to a local cache
716 | func saveMetadata(db string, meta metaData) (err error) {
717 | // Create the metadata directory if needed
718 | if _, err = os.Stat(filepath.Join(".dio", db)); os.IsNotExist(err) {
719 | // We create the "db" directory instead, as that'll be needed anyway and MkdirAll() ensures the .dio/
720 | // directory will be created on the way through
721 | err = os.MkdirAll(filepath.Join(".dio", db, "db"), 0770)
722 | if err != nil {
723 | return
724 | }
725 | }
726 |
727 | // Serialise the metadata to JSON
728 | var jsonString []byte
729 | jsonString, err = json.MarshalIndent(meta, "", " ")
730 | if err != nil {
731 | return
732 | }
733 |
734 | // Write the updated metadata to disk
735 | mdFile := filepath.Join(".dio", db, "metadata.json")
736 | err = ioutil.WriteFile(mdFile, jsonString, 0644)
737 | return err
738 | }
739 |
740 | // Saves metadata to the local cache, merging in with any existing metadata
741 | func updateMetadata(db string, saveMeta bool) (mergedMeta metaData, err error) {
742 | // Check for existing metadata file, loading it if present
743 | var md []byte
744 | origMeta := metaData{}
745 | md, err = ioutil.ReadFile(filepath.Join(".dio", db, "metadata.json"))
746 | if err == nil {
747 | err = json.Unmarshal([]byte(md), &origMeta)
748 | if err != nil {
749 | return
750 | }
751 | }
752 |
753 | // Download the latest database metadata
754 | _, err = fmt.Fprintln(fOut, "Updating metadata")
755 | if err != nil {
756 | return
757 | }
758 | newMeta, _, err := retrieveMetadata(db)
759 | if err != nil {
760 | return
761 | }
762 |
763 | // If we have existing local metadata, then merge the metadata from DBHub.io with it
764 | if len(origMeta.Commits) > 0 {
765 | mergedMeta, err = mergeMetadata(origMeta, newMeta)
766 | if err != nil {
767 | return
768 | }
769 | } else {
770 | // No existing metadata, so just copy across the remote metadata
771 | mergedMeta = newMeta
772 |
773 | // Use the remote default branch as the initial active (local) branch
774 | mergedMeta.ActiveBranch = newMeta.DefBranch
775 | }
776 |
777 | // Serialise the updated metadata to JSON
778 | var jsonString []byte
779 | jsonString, err = json.MarshalIndent(mergedMeta, "", " ")
780 | if err != nil {
781 | errMsg := fmt.Sprintf("Error when JSON marshalling the merged metadata: %v\n", err)
782 | log.Print(errMsg)
783 | return
784 | }
785 |
786 | // If requested, write the updated metadata to disk
787 | if saveMeta {
788 | if _, err = os.Stat(filepath.Join(".dio", db)); os.IsNotExist(err) {
789 | err = os.MkdirAll(filepath.Join(".dio", db), 0770)
790 | if err != nil {
791 | return
792 | }
793 | }
794 | mdFile := filepath.Join(".dio", db, "metadata.json")
795 | err = ioutil.WriteFile(mdFile, []byte(jsonString), 0644)
796 | }
797 | return
798 | }
799 |
--------------------------------------------------------------------------------
/cmd/status.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | // Displays whether a database has been modified since the last commit
11 | var statusCmd = &cobra.Command{
12 | Use: "status [database name]",
13 | Short: "Displays whether a database has been modified since the last commit",
14 | RunE: func(cmd *cobra.Command, args []string) error {
15 | return status(args)
16 | },
17 | }
18 |
19 | func init() {
20 | RootCmd.AddCommand(statusCmd)
21 | }
22 |
23 | func status(args []string) error {
24 | var db string
25 | var err error
26 | if len(args) == 0 {
27 | // TODO: If no database name is given, we should show the status for all known databases (eg in local .dio cache)
28 | // in the current directory instead
29 | db, err = getDefaultDatabase()
30 | if err != nil {
31 | return err
32 | }
33 | if db == "" {
34 | // No database name was given on the command line, and we don't have a default database selected
35 | return errors.New("No database file specified")
36 | }
37 | } else {
38 | db = args[0]
39 | }
40 | // TODO: Allow giving multiple database files on the command line. Hopefully just needs turning this
41 | // TODO into a for loop
42 | if len(args) > 1 {
43 | return errors.New("Only one database can be worked with at a time (for now)")
44 | }
45 |
46 | // If there is a local metadata cache for the requested database, use that. Otherwise, retrieve it from the
47 | // server first (without storing it)
48 | var meta metaData
49 | meta, err = localFetchMetadata(db, true)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | // Check if the file has changed, and let the user know
55 | changed, err := dbChanged(db, meta)
56 | if err != nil {
57 | return err
58 | }
59 | if changed {
60 | _, err = fmt.Fprintf(fOut, " * '%s': has been changed\n", db)
61 | if err != nil {
62 | return err
63 | }
64 | return nil
65 | }
66 | _, err = fmt.Fprintf(fOut, " * '%s': unchanged\n", db)
67 | return err
68 | }
69 |
--------------------------------------------------------------------------------
/cmd/tag.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | var tagCmd = &cobra.Command{
8 | Use: "tag",
9 | Short: "Create and remove tags for a database",
10 | }
11 |
12 | func init() {
13 | RootCmd.AddCommand(tagCmd)
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/tagCreate.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/spf13/cobra"
9 | "github.com/spf13/viper"
10 | )
11 |
12 | var (
13 | tagCreateCommit, tagCreateDate, tagCreateEmail string
14 | tagCreateMsg, tagCreateName, tagCreateTag string
15 | )
16 |
17 | // Creates a tag for a database
18 | var tagCreateCmd = &cobra.Command{
19 | Use: "create [database name] --tag xxx --commit yyy",
20 | Short: "Create a tag for a database",
21 | RunE: func(cmd *cobra.Command, args []string) error {
22 | return tagCreate(args)
23 | },
24 | }
25 |
26 | func init() {
27 | tagCmd.AddCommand(tagCreateCmd)
28 | tagCreateCmd.Flags().StringVar(&tagCreateCommit, "commit", "", "Commit ID for the new tag")
29 | tagCreateCmd.Flags().StringVar(&tagCreateDate, "date", "", "Custom timestamp (RFC3339 format) for tag")
30 | tagCreateCmd.Flags().StringVar(&tagCreateEmail, "email", "", "Email address of tagger")
31 | tagCreateCmd.Flags().StringVar(&tagCreateMsg, "message", "", "Description / message for the tag")
32 | tagCreateCmd.Flags().StringVar(&tagCreateName, "name", "", "Name of tagger")
33 | tagCreateCmd.Flags().StringVar(&tagCreateTag, "tag", "", "Name of tag to create")
34 | }
35 |
36 | func tagCreate(args []string) error {
37 | // Ensure a database file was given
38 | var db string
39 | var err error
40 | var meta metaData
41 | if len(args) == 0 {
42 | db, err = getDefaultDatabase()
43 | if err != nil {
44 | return err
45 | }
46 | if db == "" {
47 | // No database name was given on the command line, and we don't have a default database selected
48 | return errors.New("No database file specified")
49 | }
50 | } else {
51 | db = args[0]
52 | }
53 | if len(args) > 1 {
54 | return errors.New("Only one database can be changed at a time (for now)")
55 | }
56 |
57 | // Ensure a new tag name and commit ID were given
58 | if tagCreateTag == "" {
59 | return errors.New("No tag name given")
60 | }
61 | if tagCreateCommit == "" {
62 | return errors.New("No commit ID given")
63 | }
64 |
65 | // Make sure we have the email and name of the tag creator. Either by loading it from the config file, or
66 | // getting it from the command line arguments
67 | if tagCreateEmail == "" {
68 | if viper.IsSet("user.email") == false {
69 | return errors.New("No email address provided")
70 | }
71 | tagCreateEmail = viper.GetString("user.email")
72 | }
73 |
74 | if tagCreateName == "" {
75 | if viper.IsSet("user.name") == false {
76 | return errors.New("No name provided")
77 | }
78 | tagCreateName = viper.GetString("user.name")
79 | }
80 |
81 | // If a date was given, parse it to ensure the format is correct. Warn the user if it isn't,
82 | tagTimeStamp := time.Now()
83 | if tagCreateDate != "" {
84 | tagTimeStamp, err = time.Parse(time.RFC3339, tagCreateDate)
85 | if err != nil {
86 | return err
87 | }
88 | }
89 |
90 | // Load the metadata
91 | meta, err = loadMetadata(db)
92 | if err != nil {
93 | return err
94 | }
95 |
96 | // Ensure a tag with the same name doesn't already exist
97 | if _, ok := meta.Tags[tagCreateTag]; ok == true {
98 | return errors.New("A tag with that name already exists")
99 | }
100 |
101 | // Generate the new tag info locally
102 | newTag := tagEntry{
103 | Commit: tagCreateCommit,
104 | Date: tagTimeStamp,
105 | Description: tagCreateMsg,
106 | TaggerEmail: tagCreateEmail,
107 | TaggerName: tagCreateName,
108 | }
109 |
110 | // Add the new tag to the local metadata cache
111 | meta.Tags[tagCreateTag] = newTag
112 |
113 | // Save the updated metadata back to disk
114 | err = saveMetadata(db, meta)
115 | if err != nil {
116 | return err
117 | }
118 |
119 | _, err = fmt.Fprintln(fOut, "Tag creation succeeded")
120 | return err
121 | }
122 |
--------------------------------------------------------------------------------
/cmd/tagList.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "sort"
7 | "time"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // Displays the list of tags for a remote database
13 | var tagListCmd = &cobra.Command{
14 | Use: "tags [database name]",
15 | Short: "Displays a list of tags for a database",
16 | RunE: func(cmd *cobra.Command, args []string) error {
17 | return tagList(args)
18 | },
19 | }
20 |
21 | func init() {
22 | RootCmd.AddCommand(tagListCmd)
23 | }
24 |
25 | func tagList(args []string) error {
26 | // Ensure a database file was given
27 | var db string
28 | var err error
29 | var meta metaData
30 | if len(args) == 0 {
31 | db, err = getDefaultDatabase()
32 | if err != nil {
33 | return err
34 | }
35 | if db == "" {
36 | // No database name was given on the command line, and we don't have a default database selected
37 | return errors.New("No database file specified")
38 | }
39 | } else {
40 | db = args[0]
41 | }
42 | if len(args) > 1 {
43 | return errors.New("Only one database can be worked with at a time (for now)")
44 | }
45 |
46 | // If there is a local metadata cache for the requested database, use that. Otherwise, retrieve it from the
47 | // server first (without storing it)
48 | meta, err = localFetchMetadata(db, true)
49 | if err != nil {
50 | return err
51 | }
52 |
53 | if len(meta.Tags) == 0 {
54 | _, err = fmt.Fprintf(fOut, "Database %s has no tags\n", db)
55 | return err
56 | }
57 |
58 | // Sort the list alphabetically
59 | var sortedKeys []string
60 | for k := range meta.Tags {
61 | sortedKeys = append(sortedKeys, k)
62 | }
63 | sort.Strings(sortedKeys)
64 |
65 | // Display the list of tags
66 | _, err = fmt.Fprintf(fOut, "Tags for %s:\n\n", db)
67 | if err != nil {
68 | return err
69 | }
70 | for _, i := range sortedKeys {
71 | _, err = fmt.Fprintf(fOut, " * '%s' : commit %s\n\n", i, meta.Tags[i].Commit)
72 | if err != nil {
73 | return err
74 | }
75 | _, err = fmt.Fprintf(fOut, " Author: %s <%s>\n", meta.Tags[i].TaggerName, meta.Tags[i].TaggerEmail)
76 | if err != nil {
77 | return err
78 | }
79 | _, err = fmt.Fprintf(fOut, " Date: %s\n", meta.Tags[i].Date.Format(time.UnixDate))
80 | if err != nil {
81 | return err
82 | }
83 | if meta.Tags[i].Description != "" {
84 | _, err = fmt.Fprintf(fOut, " Message: %s\n\n", meta.Tags[i].Description)
85 | if err != nil {
86 | return err
87 | }
88 | } else {
89 | _, err = fmt.Fprintln(fOut)
90 | if err != nil {
91 | return err
92 | }
93 | }
94 | }
95 | return nil
96 | }
97 |
--------------------------------------------------------------------------------
/cmd/tagRemove.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var tagRemoveTag string
11 |
12 | // Removes a tag from a database
13 | var tagRemoveCmd = &cobra.Command{
14 | Use: "remove [database name] --tag xxx",
15 | Short: "Remove a tag from a database",
16 | RunE: func(cmd *cobra.Command, args []string) error {
17 | return tagRemove(args)
18 | },
19 | }
20 |
21 | func init() {
22 | tagCmd.AddCommand(tagRemoveCmd)
23 | tagRemoveCmd.Flags().StringVar(&tagRemoveTag, "tag", "", "Name of remote tag to remove")
24 | }
25 |
26 | func tagRemove(args []string) error {
27 | // Ensure a database file was given
28 | var db string
29 | var err error
30 | var meta metaData
31 | if len(args) == 0 {
32 | db, err = getDefaultDatabase()
33 | if err != nil {
34 | return err
35 | }
36 | if db == "" {
37 | // No database name was given on the command line, and we don't have a default database selected
38 | return errors.New("No database file specified")
39 | }
40 | } else {
41 | db = args[0]
42 | }
43 | if len(args) > 1 {
44 | return errors.New("Only one database can be changed at a time (for now)")
45 | }
46 |
47 | // Ensure a tag name was given
48 | if tagRemoveTag == "" {
49 | return errors.New("No tag name given")
50 | }
51 |
52 | // Load the metadata
53 | meta, err = loadMetadata(db)
54 | if err != nil {
55 | return err
56 | }
57 |
58 | // Check if the tag exists
59 | if _, ok := meta.Tags[tagRemoveTag]; ok != true {
60 | return errors.New("A tag with that name doesn't exist")
61 | }
62 |
63 | // Remove the tag
64 | delete(meta.Tags, tagRemoveTag)
65 |
66 | // Save the updated metadata back to disk
67 | err = saveMetadata(db, meta)
68 | if err != nil {
69 | return err
70 | }
71 |
72 | _, err = fmt.Fprintf(fOut, "Tag '%s' removed\n", tagRemoveTag)
73 | return err
74 | }
75 |
--------------------------------------------------------------------------------
/cmd/types.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import "time"
4 |
5 | type branchEntry struct {
6 | Commit string `json:"commit"`
7 | CommitCount int `json:"commit_count"`
8 | Description string `json:"description"`
9 | }
10 |
11 | type commitEntry struct {
12 | AuthorEmail string `json:"author_email"`
13 | AuthorName string `json:"author_name"`
14 | CommitterEmail string `json:"committer_email"`
15 | CommitterName string `json:"committer_name"`
16 | ID string `json:"id"`
17 | Message string `json:"message"`
18 | OtherParents []string `json:"other_parents"`
19 | Parent string `json:"parent"`
20 | Timestamp time.Time `json:"timestamp"`
21 | Tree dbTree `json:"tree"`
22 | }
23 |
24 | type dbListEntry struct {
25 | CommitID string `json:"commit_id"`
26 | DefBranch string `json:"default_branch"`
27 | LastModified string `json:"last_modified"`
28 | Licence string `json:"licence"`
29 | Name string `json:"name"`
30 | OneLineDesc string `json:"one_line_description"`
31 | Public bool `json:"public"`
32 | RepoModified string `json:"repo_modified"`
33 | SHA256 string `json:"sha256"`
34 | Size int64 `json:"size"`
35 | Type string `json:"type"`
36 | URL string `json:"url"`
37 | }
38 |
39 | type dbTreeEntryType string
40 |
41 | const (
42 | TREE dbTreeEntryType = "tree"
43 | DATABASE = "db"
44 | LICENCE = "licence"
45 | )
46 |
47 | type dbTree struct {
48 | ID string `json:"id"`
49 | Entries []dbTreeEntry `json:"entries"`
50 | }
51 | type dbTreeEntry struct {
52 | EntryType dbTreeEntryType `json:"entry_type"`
53 | LastModified time.Time `json:"last_modified"`
54 | LicenceSHA string `json:"licence"`
55 | Name string `json:"name"`
56 | Sha256 string `json:"sha256"`
57 | Size int64 `json:"size"`
58 | }
59 |
60 | type defaultSettings struct {
61 | SelectedDatabase string `json:"selected_database"`
62 | }
63 |
64 | type licenceEntry struct {
65 | FileFormat string `json:"file_format"`
66 | FullName string `json:"full_name"`
67 | Order int `json:"order"`
68 | Sha256 string `json:"sha256"`
69 | URL string `json:"url"`
70 | }
71 |
72 | type metaData struct {
73 | ActiveBranch string `json:"active_branch"` // The local branch
74 | Branches map[string]branchEntry `json:"branches"`
75 | Commits map[string]commitEntry `json:"commits"`
76 | DefBranch string `json:"default_branch"` // The default branch *on the server*
77 | Releases map[string]releaseEntry `json:"releases"`
78 | Tags map[string]tagEntry `json:"tags"`
79 | }
80 |
81 | type releaseEntry struct {
82 | Commit string `json:"commit"`
83 | Date time.Time `json:"date"`
84 | Description string `json:"description"`
85 | ReleaserEmail string `json:"email"`
86 | ReleaserName string `json:"name"`
87 | Size int64 `json:"size"`
88 | }
89 |
90 | type tagEntry struct {
91 | Commit string `json:"commit"`
92 | Date time.Time `json:"date"`
93 | Description string `json:"description"`
94 | TaggerEmail string `json:"email"`
95 | TaggerName string `json:"name"`
96 | }
97 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | // Displays the version number of dio
10 | var versionCmd = &cobra.Command{
11 | Use: "version",
12 | Short: "Displays the version of dio being run",
13 | RunE: func(cmd *cobra.Command, args []string) error {
14 | fmt.Printf("dio version %s\n", DIO_VERSION)
15 | return nil
16 | },
17 | }
18 |
19 | func init() {
20 | RootCmd.AddCommand(versionCmd)
21 | }
22 |
--------------------------------------------------------------------------------
/config/config.toml:
--------------------------------------------------------------------------------
1 | [certs]
2 | cachain = "/path/to/ca-chain-docker.cert.pem"
3 | cert = "/path/to/your.cert.pem"
4 |
5 | [general]
6 | cloud = "https://db4s.dbhub.io"
7 |
8 | [user]
9 | name = "Some One"
10 | email = "someone@example.org"
11 |
--------------------------------------------------------------------------------
/dio.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/sqlitebrowser/dio/cmd"
4 |
5 | func main() {
6 | cmd.Execute()
7 | }
8 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/sqlitebrowser/dio
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/mitchellh/go-homedir v1.1.0
7 | github.com/parnurzeal/gorequest v0.2.16
8 | github.com/pkg/errors v0.9.1
9 | github.com/spf13/cobra v1.8.0
10 | github.com/spf13/viper v1.18.2
11 | golang.org/x/text v0.14.0
12 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
13 | )
14 |
15 | require (
16 | github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 // indirect
17 | github.com/fsnotify/fsnotify v1.7.0 // indirect
18 | github.com/hashicorp/hcl v1.0.0 // indirect
19 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
20 | github.com/kr/pretty v0.3.1 // indirect
21 | github.com/kr/text v0.2.0 // indirect
22 | github.com/magiconair/properties v1.8.7 // indirect
23 | github.com/mitchellh/mapstructure v1.5.0 // indirect
24 | github.com/pelletier/go-toml/v2 v2.1.1 // indirect
25 | github.com/rogpeppe/go-internal v1.12.0 // indirect
26 | github.com/sagikazarmark/locafero v0.4.0 // indirect
27 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect
28 | github.com/smartystreets/goconvey v1.6.4 // indirect
29 | github.com/sourcegraph/conc v0.3.0 // indirect
30 | github.com/spf13/afero v1.11.0 // indirect
31 | github.com/spf13/cast v1.6.0 // indirect
32 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
33 | github.com/spf13/pflag v1.0.5 // indirect
34 | github.com/subosito/gotenv v1.6.0 // indirect
35 | go.uber.org/atomic v1.11.0 // indirect
36 | go.uber.org/multierr v1.11.0 // indirect
37 | golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
38 | golang.org/x/net v0.20.0 // indirect
39 | golang.org/x/sys v0.16.0 // indirect
40 | gopkg.in/ini.v1 v1.67.0 // indirect
41 | gopkg.in/yaml.v3 v3.0.1 // indirect
42 | moul.io/http2curl v1.0.0 // indirect
43 | )
44 |
--------------------------------------------------------------------------------
/misc/.gitignore:
--------------------------------------------------------------------------------
1 | dio*
2 |
--------------------------------------------------------------------------------
/misc/build_binaries.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # This is just a small sh script to generate the Dio release binaries
4 |
5 | export GOARCH=386
6 | for GOOS in android darwin freebsd netbsd openbsd plan9 windows linux; do
7 | echo Building Dio for ${GOOS}-${GOARCH}
8 | go build -o dio-${GOOS}-x86 ..
9 | sha256sum dio-${GOOS}-x86 > dio-${GOOS}-x86.SHA256
10 | done
11 |
12 | export GOARCH=amd64
13 | for GOOS in android darwin freebsd netbsd openbsd plan9 solaris windows linux; do
14 | echo Building Dio for ${GOOS}-${GOARCH}
15 | go build -o dio-${GOOS}-${GOARCH} ..
16 | sha256sum dio-${GOOS}-${GOARCH} > dio-${GOOS}-${GOARCH}.SHA256
17 | done
18 |
19 | export GOARCH=arm
20 | for GOOS in android darwin freebsd netbsd openbsd plan9 windows linux; do
21 | echo Building Dio for ${GOOS}-${GOARCH}
22 | go build -o dio-${GOOS}-${GOARCH} ..
23 | sha256sum dio-${GOOS}-${GOARCH} > dio-${GOOS}-${GOARCH}.SHA256
24 | done
25 |
26 | export GOARCH=arm64
27 | for GOOS in android darwin freebsd illumos netbsd openbsd linux; do
28 | echo Building Dio for ${GOOS}-${GOARCH}
29 | go build -o dio-${GOOS}-${GOARCH} ..
30 | sha256sum dio-${GOOS}-${GOARCH} > dio-${GOOS}-${GOARCH}.SHA256
31 | done
32 |
33 | GOOS=linux
34 | for GOARCH in mips mips64 mips64le mipsle ppc64 ppc64le s390x; do
35 | echo Building Dio for ${GOOS}-${GOARCH}
36 | go build -o dio-${GOOS}-${GOARCH} ..
37 | sha256sum dio-${GOOS}-${GOARCH} > dio-${GOOS}-${GOARCH}.SHA256
38 | done
39 |
40 | echo Building Dio for ${GOOS}-ARMv6
41 | GOARCH=arm GOARM=6 go build -o dio-${GOOS}-armv6 ..
42 | sha256sum dio-${GOOS}-armv6 > dio-${GOOS}-armv6.SHA256
43 |
44 | echo Building Dio for aix-ppc64
45 | go build -o dio-aix-ppc64 ..
46 | sha256sum dio-aix-ppc64 > dio-aix-ppc64.SHA256
47 |
48 | echo Building Dio for js-wasm
49 | go build -o dio-js-wasm ..
50 | sha256sum dio-js-wasm > dio-js-wasm.SHA256
51 |
--------------------------------------------------------------------------------
/test_data/19kB.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sqlitebrowser/dio/b587368e5c6bdfb3a84e0b0270f62b93873d41cc/test_data/19kB.sqlite
--------------------------------------------------------------------------------
/test_data/ca-chain-docker.cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIGSjCCBDKgAwIBAgICEAMwDQYJKoZIhvcNAQELBQAwga8xCzAJBgNVBAYTAkdC
3 | MRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhEQkh1Yi5pbzEnMCUGA1UECwwe
4 | REJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSwwKgYDVQQDDCNEQkh1Yi5p
5 | byBEb2NrZXIgRGV2ZWxvcG1lbnQgUm9vdCBDQTEkMCIGCSqGSIb3DQEJARYVanVz
6 | dGluQHBvc3RncmVzcWwub3JnMB4XDTIxMDUwMjExMjkxM1oXDTMxMDUwMzExMjkx
7 | M1owgbcxCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhE
8 | Qkh1Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5
9 | MTQwMgYDVQQDDCtEQkh1Yi5pbyBEb2NrZXIgRGV2ZWxvcG1lbnQgSW50ZXJtZWRp
10 | YXRlIENBMSQwIgYJKoZIhvcNAQkBFhVqdXN0aW5AcG9zdGdyZXNxbC5vcmcwggIi
11 | MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGwn/4kHEK3wBol8/fTLZI15Ie
12 | HH88Vk8ks++S7FTg7GeN3ITNHvvEFQ9qlV3OZRMckvP/UBVbqupnewQjOQO3f6H+
13 | FW7Pfn9bynZmCb5uaWhzxdqLJye9jSEw57tAkAwXEY2RSFJTYr4UVU3Lmow+Iqj/
14 | sAOXsPTIKkIIUSzC+khla6eXyZzeK0/uroQQYHGIJRLuihP33xQ520GRpVdLDeKr
15 | JJIw85YitMpdm0RfH1kEDPrQZVtC8XMjpA3G+4BrYcJazO8s+txwQQxgT5SOkvT1
16 | XNUlGSLMKFvY2Ufy5J1mouGU4H90b82tf30cfyHDRjQlVMHAhN+AJA1jdj/aJBl5
17 | HUNOB0tpL2OSYymEgyqnDyt1crMKjZU8PEM7LgbeRTHj8p2NeFPMJXOIhsN6gw0W
18 | 8+lbau70RPmPyFki0umM45PFomwKdYAO3GnC4vDWyoU1aPA82lzbTKTlbNQj5idM
19 | vrPZW0LCRZ0rnwHyCLqYTItYUlSItVBJnVJ77z1xVi1bNNHBBy8/1etM+LKMWLMu
20 | HmTKZK1dcpOqVZ9oIg3fvL3/8kdkaFVuloj6sU0SpNwDiF839O0sv5a0URrX+23K
21 | wjmtLNmXfZtz6ikSfc87mCZePHRqVejv1lQJYf9a5aw9Xm3FTSJDLPThNttsjCKg
22 | jVDRnvlT22ywtLNOlQIDAQABo2YwZDAdBgNVHQ4EFgQU/sG8zdpEuNy3LzZho0MA
23 | bWZ/qoowHwYDVR0jBBgwFoAUQRp62y8gTl4irm8Q2gz7NNf9WSAwEgYDVR0TAQH/
24 | BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAJkN
25 | 0y0PnBdC6OwKHEIFXVDauiKvVTdm5/1AGtvE7C1t8cnVb22Jygzaw7TyekYqTaTV
26 | sVW8zXCgNMNvTZOfB17A1jn7zxXjHYi3/IZw5NAzz02SPutxWgetuas4EdDwD9iT
27 | thHxkq2c6/1LaY/ZVHuQvnrIIfec01ZK6LzAlQyD8/v6CIoBTBqEIerVo9YNTimd
28 | l/UF4DVnX17jyZKWJuKqyL8HCC42QqC9smGPGvnE8mVdo0ed40+Dsx82n2vWLNVr
29 | nltNTCww4ryRcmtsuEsdRv1b+MJLJfFVEm9nevXZplAs0XwjEtETvlFWvPQz+zOH
30 | hm6LglP3LXLYIIzHnwSV9e9Qwr4yXAReBZxnfVdUzw0JDZdVUniA7sUSMlRQIg1F
31 | KXU42sT1AcCOuWUaD72MF29xpfOt5pWOM5R76y3xC6bhuRwkneUX9Nf5iHAkwG23
32 | MwpurAVZiO5VR/LCYuL1vPP2XOmC5a10qbCQzP5hACCTXr5P8xXVtB4tyMZ46Vnw
33 | wnGxZ9bxhiXU8OO/FISWKmb9XFbbiNcJyRFQSjVU1prypvRpbbmwVg9bL1JgemH+
34 | VQK3XIKr3GHWqAwRVGcNYD4LVLO0HX5kKkeIB2NzfvR8Bxn1WIjJPPZFyc4gAoLg
35 | v57ad/lF+jA0wlW68dB8AGV0HKjxq6b9jAeR+W2h
36 | -----END CERTIFICATE-----
37 | -----BEGIN CERTIFICATE-----
38 | MIIGUTCCBDmgAwIBAgIUROzwAIHBoAT1lP4XR65lSC/C+uEwDQYJKoZIhvcNAQEL
39 | BQAwga8xCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhE
40 | Qkh1Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5
41 | MSwwKgYDVQQDDCNEQkh1Yi5pbyBEb2NrZXIgRGV2ZWxvcG1lbnQgUm9vdCBDQTEk
42 | MCIGCSqGSIb3DQEJARYVanVzdGluQHBvc3RncmVzcWwub3JnMB4XDTIxMDUwMjEx
43 | MTk0MVoXDTMxMDUwMzExMTk0MVowga8xCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdF
44 | bmdsYW5kMREwDwYDVQQKDAhEQkh1Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2Vy
45 | dGlmaWNhdGUgQXV0aG9yaXR5MSwwKgYDVQQDDCNEQkh1Yi5pbyBEb2NrZXIgRGV2
46 | ZWxvcG1lbnQgUm9vdCBDQTEkMCIGCSqGSIb3DQEJARYVanVzdGluQHBvc3RncmVz
47 | cWwub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx45dv4Ue1YZ8
48 | 7NxY26n3ccMhmtifRatdyCV0XWIIe8zBYQ3R1qSGIDmrNjNSA2i+DVIcktEsWO+B
49 | xPPGneI0NPv1sKnhWBpC93hVm0q8cCh07ud37BsYZEOu+j04Z3hZmJ+LBKhC9kJA
50 | p8bsZPAJokF1ThHmfMCD8Cr0EntnZdRsY+uVpw0AsHvC2PHmQvs5f2K49R4SwVD6
51 | ehkKaM5qjb5e+TkeuIg16qKeIwyz0qiL41Z07+THw5JBbkVuThLrGQ721iydpxPR
52 | 3CJbRtJemNDdrZCEyvcgq8w6jRe519McmCUCEuYZy1rbodng5MOybUyvnLi5AwzA
53 | EeDsHk18yF0P9JhVmPGg8bSswoscYQk3p821IMZpL4DFVLdA0nHdwZeZoD7fw0TV
54 | p5h4oD5t0tsFR/N9cBEUvunwJ4P1WU1MstLrfQf4YVWzYOoZgl8nBf7zqoGwYN9t
55 | NkTgr4fnWZM8rZTzm93Mpr2nZ2r/7hMzadfrQFPRgR4BYFrZzMutUisLqMG5qMdJ
56 | 4/z3qRP6+Gwr8cZjRJw7Pv6dDJes/uq1PHdJ8c4pYR+GGmpT2rM8jHX2n8C2vJFG
57 | x19BEjNaqhY34zonTAA/WrFYcrGamzojqIyRBEcc572E0HWShC053HlMmjMHSukh
58 | 6uwQ3U6x2ABAqP76qGaj89skELSi6RkCAwEAAaNjMGEwHQYDVR0OBBYEFEEaetsv
59 | IE5eIq5vENoM+zTX/VkgMB8GA1UdIwQYMBaAFEEaetsvIE5eIq5vENoM+zTX/Vkg
60 | MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA
61 | A4ICAQAjZKP5sTXPEXdzv65XCBsEj8kVR6SDs3WTVCU/gU2Lylqe/gB0kKRh2C9K
62 | /DafSfsEYx0qwHXt4qRlTU7ih87CY+QnCw5IBaye0wVCofc1FqjP6T7RC7QNhni8
63 | mUzeAl9eOf6Idex1wzEhgk+lulrJ3igHczLfR+layr8RLY7De9pO23xPIQoV23Yo
64 | /kYJicKBKOs6U/tM4nzoZsiB9yEUIdnOkF8SGWBbYVMOkxt8CYtChHGqJFeEveuc
65 | 5uY5Ot1iPrAPVP30JeVpGu6TQmFWNjXo18eqj/lmw5G1iSAAxSfIeHb1njFlw/++
66 | irpAnICe6ggSSi8IgEeuwlZfNtTAGjXpqU3xFKvv6vg9Y7D4UlA1ln/Xd2P1Ea7t
67 | Yy0UhBs6a4SbEm7Au4m7SwEMA+ImGcbUacgiLX/EDLqUjM02WtwNeXq3dK4bIbo5
68 | DhvKvYg11LQT1A1XJQopMFRMo6YNnsWXxy3MWq3l6GAqxmet+vslTAuOUdorS/5t
69 | 0yhQo1JtjcxZV7m+7TcwvFs+oeseaBXiLHkHM9P5TKsTJ9vKQuohEc0o1+YS+TqA
70 | Oie7rmViMy8+IKE9pFGNjo9KBtRshqDpz9C5z8X4WDpM7lB+EcFKIMbz0P4vPIcZ
71 | KDeX5COGCBbVnqBR7+MD+jU5k0376ShF+BAM3Ob9qF09aw0vMg==
72 | -----END CERTIFICATE-----
73 |
--------------------------------------------------------------------------------
/test_data/default.cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEzzCCAregAwIBAgIRAM68feLtPpJimOFBbglYNk8wDQYJKoZIhvcNAQELBQAw
3 | gbcxCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhEQkh1
4 | Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5MTQw
5 | MgYDVQQDDCtEQkh1Yi5pbyBEb2NrZXIgRGV2ZWxvcG1lbnQgSW50ZXJtZWRpYXRl
6 | IENBMSQwIgYJKoZIhvcNAQkBFhVqdXN0aW5AcG9zdGdyZXNxbC5vcmcwHhcNMjEw
7 | NTAzMTIwMDI3WhcNMjYwNTAzMTIwMDI3WjBGMR4wHAYDVQQKExVEQiBCcm93c2Vy
8 | IGZvciBTUUxpdGUxJDAiBgNVBAMMG2RlZmF1bHRAZG9ja2VyLWRldi5kYmh1Yi5p
9 | bzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKrTqm5/idSi/aV5N+3s
10 | IeFz1HsPLZFvqzYKp1YMsIHyC0h6X25LYTziXRA0Ut/NbMQACxYeTjaWHIqtrUnW
11 | bcyxJtnFJD9qonv871ytq5VXtjPU26J4y9zLgvcwZZ6mYuoytVQ/7JhfV/7Ausgp
12 | UcEzAZJm3ihfji0ofNBcDLcLMlIbZSYWb0/9XD3LeJFTo2rw3QoiUj1OOEyEggKZ
13 | MWW6hILv8StNawdecvY+f0+UuohlWI2+gSurQHvnnpt5oSxUKNqpbv4r4YFT52DU
14 | saOWEF/MVrN3hpIuqKpqDZ1/vuJMRJ9Btw9TgTnnuM47NasxhOD6M1LCUrcXiL5n
15 | /L8CAwEAAaNGMEQwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAf
16 | BgNVHSMEGDAWgBT+wbzN2kS43LcvNmGjQwBtZn+qijANBgkqhkiG9w0BAQsFAAOC
17 | AgEAMSS038hWK4SxJGj44S7mWuiXrRXbUnqytbb7Ox/Tdgjz1jezuPPzCB/vY0YM
18 | rW5e+7CkR9H22IGNeG52y0a10JiB4FHBx+ByiAowy88+qZPYPHxptWKh4UJUVTJZ
19 | /3pKaY/hiEnJn8qduZILoo2IYktdR9QpQXJo7epXBRkYjUAauYnnDkHQnhxVjf4V
20 | JktJxxyQKUyhx8zlpF4BhPifGUsjPmQVnGpQ4rRkBB1AQ7X+tid2oqOqXcqjXhPM
21 | QzP42qc0BFJhekkf0otzhJFt8YgUMkqe4N13nVyldc1+QHADFIKWpsHBZ11Oa49w
22 | 83XbZBDa8L1oIW5UornKwJytzwEJ0dK4oCH9dt3QfwZ/I5+fdS8k4rXYSkOuF8gm
23 | 0HeB/W2PrcYxUbTKpi0Gx5IAfHHjGtTWjy1tKrSprN1eQke2LNDXLyUFUh3IRm2y
24 | tbvatv7+rf5wTtcnZEsjf5ulRsC+AvC6tz8JT+hcr7bOhuUZK0DW7L7G45D3EJam
25 | p0Hcwt3m1+J+iKll9To9pVnkzI3afECtPMYRpQALaq+gKSd5I8knTdYQhIdNH4gf
26 | Y+B93I7sZIjByxPQt5I84KZNzChYHQA20KPtyT9invr1qNLkvaXnfK4HzjKQ1umM
27 | 05x7EI2i3YiHzV2JlMNVg6awzSJTlD+gGzeeH5r00G6Qups=
28 | -----END CERTIFICATE-----
29 | -----BEGIN RSA PRIVATE KEY-----
30 | MIIEpAIBAAKCAQEAqtOqbn+J1KL9pXk37ewh4XPUew8tkW+rNgqnVgywgfILSHpf
31 | bkthPOJdEDRS381sxAALFh5ONpYciq2tSdZtzLEm2cUkP2qie/zvXK2rlVe2M9Tb
32 | onjL3MuC9zBlnqZi6jK1VD/smF9X/sC6yClRwTMBkmbeKF+OLSh80FwMtwsyUhtl
33 | JhZvT/1cPct4kVOjavDdCiJSPU44TISCApkxZbqEgu/xK01rB15y9j5/T5S6iGVY
34 | jb6BK6tAe+eem3mhLFQo2qlu/ivhgVPnYNSxo5YQX8xWs3eGki6oqmoNnX++4kxE
35 | n0G3D1OBOee4zjs1qzGE4PozUsJStxeIvmf8vwIDAQABAoIBADR5uQ0gmJJ9TzWZ
36 | uxiXRQEgt9DlpLXce9eqBiVk2IPSeqzVCqOy/DfbwYLMz/h3/kVnTgCJZrVV/4aK
37 | O4VHHYuXj7ut16izdR5pYI4zu1WxEAN0C9QpD1bQHXcZot3Ndu1CjnlG+cME5t8X
38 | DUmXh8m1hXIXr37ve5lbqpvG6xD/GvUVOqWTPB7hCBEzDw4DWKH1O6V5Q8eR2tlU
39 | Nhu9P4XsL6LO4NfrfCi061CWugWL67NfwrlKhcHtn4lgNkXCNrgCp+yMqKptkFbg
40 | zWUm0P+pMnuttQkENAgrTDyjhLkS4o9l7ZSp75lhUNCmXvjd2bzu2FOwpY+4ugyz
41 | US9dTeECgYEAykY8BofJZuUG43jrNy3SGZM/mWgFr+xssQ5ntLKu1U6fsx7/OpV+
42 | ClBLwBoE+1PjWX48wnBXAR7bk7gHyLppfafasyHEoVL3lB7OJrD6/Baq5HQAQxYw
43 | QWXnFyQlEUpwZG2GZKT+144wRFEl1WyBLWXG8AcPLSLzVPratDiYNo0CgYEA2DMj
44 | LNZhW2bG+lfIf6IJ3zokWYV2suMYVEl3mheq8PsIB3jQRCfjXdA8tlnIXDQtrPAs
45 | eewz4pGGKEbAWt099h5FRh+U1Erw0i+XwVLEHYlUiP3aAR2x61eF0jmEs4e1oiVR
46 | E/qSkvZeQ3IZ8Gmmq7avB12K3+6fTwyV+Kcyo3sCgYB+VhHFrmffqWp9Bxg6lZbl
47 | PG/7u9nZgFx+1dV2KihCuGHMua6GA7r+bBpz+IxmAYY9bjg65XmiDIjuoYHTIIMk
48 | 5YMWYR/z9uMFk5wE1INekjXYjI9hV2l6X1BPxtaUDx9VyoanM9qr/XYuJVTxEV05
49 | Ypk3b+FNuseqqyeQasy/PQKBgQDK8440m/Z+h8eH3/neHm1n+LuAsfHQUbBYBzNY
50 | GpmkZ/KMmRPgtxUPztf/Ud7s9ypdaoRF276FFJi8nFYbtg5hSN88yY67jrHsjTLH
51 | Dvv8whryEmKgo5COXPXJd6cjpOSTlrY6rAEGJnIsnCLPdU45aV966YvhVK6F1Um/
52 | Rq0ZmwKBgQCx1DxgbnuaFIDxSIuGpPdbFJXPkNnnq5w0x9hJ0C6f0P6DV3sUljlT
53 | uWmA5VINZLItOlxKUxGV2Q+jNLc8Mh/EcFYx3OWpN8go4ufbwIf0LxDEGmO5VVdj
54 | LUG7+4EjhzZZt6KqRx/rK4DYGwKM352xs/jmAkjw7fYVmNAyzqtSUg==
55 | -----END RSA PRIVATE KEY-----
56 |
--------------------------------------------------------------------------------
/test_data/docker-dev.dbhub.io.cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIGVjCCBD6gAwIBAgICEBAwDQYJKoZIhvcNAQELBQAwgbcxCzAJBgNVBAYTAkdC
3 | MRAwDgYDVQQIDAdFbmdsYW5kMREwDwYDVQQKDAhEQkh1Yi5pbzEnMCUGA1UECwwe
4 | REJIdWIuaW8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5MTQwMgYDVQQDDCtEQkh1Yi5p
5 | byBEb2NrZXIgRGV2ZWxvcG1lbnQgSW50ZXJtZWRpYXRlIENBMSQwIgYJKoZIhvcN
6 | AQkBFhVqdXN0aW5AcG9zdGdyZXNxbC5vcmcwHhcNMjEwNTAyMTE1MjIwWhcNMzEw
7 | NTAzMTE1MjIwWjCBpDELMAkGA1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxETAP
8 | BgNVBAoMCERCSHViLmlvMSwwKgYDVQQLDCNEQkh1Yi5pbyBEb2NrZXIgSW5mcmFz
9 | dHJ1Y3R1cmUgVGVhbTEcMBoGA1UEAwwTZG9ja2VyLWRldi5kYmh1Yi5pbzEkMCIG
10 | CSqGSIb3DQEJARYVanVzdGluQHBvc3RncmVzcWwub3JnMIIBIjANBgkqhkiG9w0B
11 | AQEFAAOCAQ8AMIIBCgKCAQEAmP3v+fI0kpZQ6XMM2zUge/lRpGzQC8ChtftVyTCM
12 | PcHmgNwjn4nFzLvCAO4eWgZp4l7Oa4X8TqJrQTO1dZSccD36esJolTIYmuJRofJh
13 | QWTT/C2C0o/qyhjHTqLjq6Wsz3LU9Nbjidp3z0PkRPH+n+yx63uW1Im3sEHUSU3N
14 | l74MlLd+Rv+GHWBs02VvbTjHs+TIVttozmhKHFDVpSeitS1HId0I0WlLYt8TRzBF
15 | WmhkMqXF65x7S3qjcsC4Aeyl0Ldc8gUGUe/P6FN75JHjbow/OchAJ1wkGUieaN82
16 | /jWWahQdkrFTbf0/k/wqJLS0C+2DTu8xlrro79D1qUfaIwIDAQABo4IBezCCAXcw
17 | CQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMCBkAwMwYJYIZIAYb4QgENBCYWJE9w
18 | ZW5TU0wgR2VuZXJhdGVkIFNlcnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU1aBS
19 | 3E1emFvU4VWDmxHBxozdXrEwgd0GA1UdIwSB1TCB0oAU/sG8zdpEuNy3LzZho0MA
20 | bWZ/qoqhgbWkgbIwga8xCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdFbmdsYW5kMREw
21 | DwYDVQQKDAhEQkh1Yi5pbzEnMCUGA1UECwweREJIdWIuaW8gQ2VydGlmaWNhdGUg
22 | QXV0aG9yaXR5MSwwKgYDVQQDDCNEQkh1Yi5pbyBEb2NrZXIgRGV2ZWxvcG1lbnQg
23 | Um9vdCBDQTEkMCIGCSqGSIb3DQEJARYVanVzdGluQHBvc3RncmVzcWwub3JnggIQ
24 | AzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcN
25 | AQELBQADggIBAIGp+wkjfLgRxqYSrivrL5OiZ3ZswQsNEhVcDSH7esYr4Dus8wCp
26 | IfHlfZ2TjJoByBO7jnZ8OifGjRupVvX/Yj7prIBcgwaB7I9Y7HzbMAB1JOt8oc/X
27 | b8lrp2zaTTb6O9tceCKyLiugO74yL8u4XEj7v+bWFwaD+JvH8l2jQze/pmvji0E4
28 | L1sQRIBqjhv5FpXBEuWe0Ze+37F2X5iB8zujy5361Kd0yV+xwFqUs8TGyPV5W8ap
29 | FHeRXJKtsz8NG3I6tnybAGq/eWcZbaJYrBbLv8qXYd0O7VyYwIIfwskTs77LOjBO
30 | 5VmBSqgcwImL26/9L0Wo3PwWzkJ+vbkwJtb0+d+sxz20fK0aWh5rIeFc56GptgLI
31 | iGyryfjvv4oiHVamjamhZQK59X09nIvk4exu8w79+vcSzj1SCNdEz2xjcUS3rgEf
32 | +KCbqJOyIS5xjzC0EL4wgkphZTPG4QjKwbEL1OJwFd0mi3VIPA+i6jDGsSc7hVJD
33 | kqLFyG9d7JgjBZS4NkLgdyCIoETKWoJg57weffBblHw7gJG9EVGQ2be3NAENnE58
34 | Mzz/O0yRfYWPmfNJOGFqppDywDw8DfN7m6H+5ivs3fEAaPif1qznuFfVMmkhcA25
35 | nAQfrXhZ0vEW4RTT3lBqDJyq/jnG2NBEvorTpqy9/m1tZdLync08BPrj
36 | -----END CERTIFICATE-----
37 |
--------------------------------------------------------------------------------
/test_data/docker-dev.dbhub.io.key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEowIBAAKCAQEAmP3v+fI0kpZQ6XMM2zUge/lRpGzQC8ChtftVyTCMPcHmgNwj
3 | n4nFzLvCAO4eWgZp4l7Oa4X8TqJrQTO1dZSccD36esJolTIYmuJRofJhQWTT/C2C
4 | 0o/qyhjHTqLjq6Wsz3LU9Nbjidp3z0PkRPH+n+yx63uW1Im3sEHUSU3Nl74MlLd+
5 | Rv+GHWBs02VvbTjHs+TIVttozmhKHFDVpSeitS1HId0I0WlLYt8TRzBFWmhkMqXF
6 | 65x7S3qjcsC4Aeyl0Ldc8gUGUe/P6FN75JHjbow/OchAJ1wkGUieaN82/jWWahQd
7 | krFTbf0/k/wqJLS0C+2DTu8xlrro79D1qUfaIwIDAQABAoIBAQCMNf3elcj0puoU
8 | aSpZI4FX3RCjnk2015/chjECp4l/d9rmMdo79infDhwoehI68zHLEpQfGrY24sdl
9 | BBzDW6VbgJG0O8NZKIZAPDYQM3BKXDujG5qPmvHUsYzHVqVMxBNyM41TrjTuO9gd
10 | jd0ACsAOlQAiDiwXMPe1gz8oxzMqYsmYrvU+aWUgJwOG9ITrfBVUN1ofPkLBv88M
11 | BBVYg2pG6XvBcWwIkK4VPfoBMC+Q48oaMdInh44XMzxyOGsS4Mn4+51WRdv3GkIb
12 | 0cpuxioitrrGdUN73uB2CgEpJTXKR6CSMoDuZKa4jF6wX5RuRHRXDKlX0V/UxFxt
13 | ppiHAcSRAoGBAMm4SQCtgIS5OrF+97roPKKf0xSc37i0J9tsgLAdTsrxIr9CIliu
14 | PjaIBXgt+C/es+soar+nrNLRD7uRDB0Sy7aVhzQerCJ3e3k8sdoqPZsghWVFkuzb
15 | +z1X7S3Gvzu09OFTniFptR2/+TBT0czWVYstG+ZBxgPG6FQWT5Qaga9VAoGBAMIo
16 | +JAKc62l8GdYqBCIfiYd+vLbSW57D/MzhUukKYqIPhwcicbLrZSleB00D97afe29
17 | o1Zsm5/+OgcVb+i5L6CNAh39y1SuCmy5Gb3BC0QZNILLTIeE9cw/p5FAcwgz9gMJ
18 | Z76GImueKgOV/255pZ7rjNgvGjp6iK1f6D7qbbOXAoGAKWuGyfXWauph6+pnUeC3
19 | +qiYviXMJnAPsxWfgwoxkKhc+yrIRK9apPXfMaM20BWJmiLNcJcsfIljEp+g/iNK
20 | 4y3m+kPGErm4B9f3qRV9WuodmgLkPXCaMSlp0Tl7MPZiRhZWZQQApaAyucKsVMQ6
21 | An77uJcO4t2n/QQryPx8XpUCgYAvQOVtuP16T554qH0OuQlqoXVH0dLHTrANEobo
22 | Z+WsT4g+Mzvc5Ak02iingtox9J8dU0ADcp9Vivv4aWE5FIjg3DCdt/zaeRkUaOA4
23 | 7FiflDrRckUH3nYr5XoUwci9QFgpWQqkteR+qJm1EbZ+3qBOUymOG7iYbuYAvAy7
24 | 8zYLtQKBgEFAJh1EXTllGZ7dWJHr+nft75Ss6uDuHeULlN8QPL7W49iGFzsLvbeT
25 | kFp94BhBeST0TuyxWs893U7hVpVOfCei7DFWXTnfeJuCeQTkZrCDY5g65pds9B1k
26 | Dp2ywFSXEeNJN5qpReli+DClPcNpHOpmRQQWbPmKBRCorUmQNr1E
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------