├── .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 | --------------------------------------------------------------------------------