├── .gitignore ├── LICENSE ├── Makefile ├── Makefile.waterlog ├── README.md ├── TODO.md ├── cli ├── latest.go ├── quick.go ├── releases.go └── root.go ├── config └── config.go ├── cuppa.go ├── go.mod ├── go.sum ├── providers ├── cpan │ ├── module.go │ ├── provider.go │ ├── release.go │ └── releases.go ├── git │ └── provider.go ├── github │ ├── graphql.go │ └── provider.go ├── gitlab │ ├── provider.go │ ├── tag.go │ └── tags.go ├── gnome │ └── provider.go ├── gnu │ └── provider.go ├── hackage │ ├── provider.go │ ├── release.go │ └── releases.go ├── html │ ├── config.go │ ├── configs.go │ ├── provider.go │ ├── upstream.go │ └── upstreams.go ├── jetbrains │ ├── provider.go │ ├── release.go │ └── releases.go ├── kde │ ├── listing.go │ └── provider.go ├── launchpad │ ├── files.go │ ├── provider.go │ ├── releases.go │ ├── series.go │ └── versions.go ├── provider.go ├── pypi │ ├── provider.go │ ├── releases.go │ └── source.go ├── rubygems │ ├── latest.go │ ├── provider.go │ ├── version.go │ └── versions.go └── sourceforge │ └── provider.go ├── results ├── result.go ├── resultset.go └── status.go ├── util └── http.go └── version ├── version.go └── version_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | cuppa 2 | 3 | .todo 4 | 5 | test.sh 6 | *.test 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKGNAME = cuppa 2 | DESTDIR ?= 3 | PREFIX ?= /usr 4 | BINDIR = $(PREFIX)/bin 5 | 6 | GOBIN = ~/go/bin 7 | GOPROJROOT = $(GOSRC)/$(PROJREPO) 8 | 9 | GOLDFLAGS = -ldflags "-s -w" 10 | GOTAGS = --tags "libsqlite3 linux" 11 | GOCC = go 12 | GOFMT = $(GOCC) fmt -x 13 | GOGET = $(GOCC) get $(GOLDFLAGS) 14 | GOBUILD = $(GOCC) build -v $(GOLDFLAGS) $(GOTAGS) 15 | GOTEST = $(GOCC) test 16 | GOVET = $(GOCC) vet 17 | GOINSTALL = $(GOCC) install $(GOLDFLAGS) 18 | 19 | include Makefile.waterlog 20 | 21 | GOLINT = $(GOBIN)/golint -set_exit_status 22 | 23 | all: build 24 | 25 | build: setup-deps 26 | @$(call stage,BUILD) 27 | @$(GOBUILD) 28 | @$(call pass,BUILD) 29 | 30 | test: build 31 | @$(call stage,TEST) 32 | @$(GOTEST) ./... 33 | @$(call pass,TEST) 34 | 35 | validate: setup-deps 36 | @$(call stage,FORMAT) 37 | @$(GOFMT) ./... 38 | @$(call pass,FORMAT) 39 | @$(call stage,VET) 40 | @$(call task,Running 'go vet'...) 41 | @$(GOVET) ./... 42 | @$(call pass,VET) 43 | @$(call stage,LINT) 44 | @$(call task,Running 'golint'...) 45 | @$(GOLINT) 46 | @$(call pass,LINT) 47 | 48 | setup-deps: 49 | @$(call stage,DEPS) 50 | @if [ ! -e $(GOBIN)/golint ]; then \ 51 | $(call task,Installing golint...); \ 52 | $(GOGET) golang.org/x/lint; \ 53 | fi 54 | 55 | install: 56 | @$(call stage,INSTALL) 57 | install -D -m 00755 $(PKGNAME) $(DESTDIR)$(BINDIR)/$(PKGNAME) 58 | @$(call pass,INSTALL) 59 | 60 | uninstall: 61 | @$(call stage,UNINSTALL) 62 | rm -f $(DESTDIR)$(BINDIR)/$(PKGNAME) 63 | @$(call pass,UNINSTALL) 64 | 65 | clean: 66 | @$(call stage,CLEAN) 67 | @$(call task,Removing executable...) 68 | @rm $(PKGNAME) 69 | @$(call pass,CLEAN) 70 | -------------------------------------------------------------------------------- /Makefile.waterlog: -------------------------------------------------------------------------------- 1 | define stage 2 | printf "\033[30;48;5;004m ⮞ \033[7m %s \033[27m %-6s \033[49;38;5;004m %s\033[0m\n" $$(date +%H:%M:%S) "$1" "$2" 3 | endef 4 | 5 | define fail 6 | printf "\033[30;48;5;208m ✗ \033[7m %s \033[27m %-6s \033[49;38;5;208m %s\033[0m\n" $$(date +%H:%M:%S) "$1" "$2" 7 | exit 1 8 | endef 9 | 10 | define pass 11 | printf "\033[30;48;5;040m 🗸 \033[7m %s \033[27m %-6s \033[49;38;5;040m %s\033[0m\n" $$(date +%H:%M:%S) "$1" "$2" 12 | endef 13 | 14 | define task 15 | printf "\033[30;48;5;220m 🗲 \033[7m %s \033[27m %-6s \033[49;38;5;220m %s\033[0m\n" $$(date +%H:%M:%S) "TASK" "$1" 16 | endef 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cuppa 2 | Comprehensive Upstream Provider Polling Assistant 3 | 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/DataDrake/cuppa)](https://goreportcard.com/report/github.com/DataDrake/cuppa) [![license](https://img.shields.io/github/license/DataDrake/cuppa.svg)]() 5 | 6 | ## Motivation 7 | 8 | As a package maintainer, it's a challenging task to keep track of every new release of a piece of software. Usually this involves subscribing to mailing lists, signing up for notifications from FOSS portals like Github, or even subscribing to news sites. For a distro, this might also mean a repeated effort amongst its package maintainers. The inefficiency and time requirements of such an approach is inevitable. This has led several distros to create their own upstream tracking platforms to automate the process of tracking upstream releases. However, these platforms are often distro specific, leading to further duplication of effort between distros. 9 | 10 | ## Goals 11 | 12 | * Support as many upstream providers as possible 13 | * Be completely distro agnostic 14 | * Extensibility 15 | * A+ Rating on [Report Card](https://goreportcard.com/report/github.com/DataDrake/cuppa) 16 | 17 | ## Progress 18 | 19 | ### Supported Providers 20 | * CPAN 21 | * Github (with API Key support) 22 | * GitLab 23 | * GNOME 24 | * Hackage 25 | * Jetbrains 26 | * KDE 27 | * Launchpad 28 | * PyPi 29 | * RubyGems 30 | * Sourceforge 31 | 32 | ### Planned Providers 33 | * BitBucket 34 | * FTP 35 | * Git 36 | 37 | ### Stretch Goal Providers 38 | * RSS 39 | 40 | Both of these will require some level of scraping to get useful info. 41 | 42 | ### Unsupported Providers 43 | * NPM 44 | Completely pointless as this will just pivot to another provider 45 | * Stackage 46 | Not really in scope for this project and they seem to be missing a web API 47 | 48 | ## Installation 49 | 50 | 1. Clone repo and enter its 51 | 2. `make` 52 | 3. `sudo make install` 53 | 54 | ## Configuration 55 | 56 | Your configuration file must be located at `$HOME/.config/cuppa` 57 | 58 | ### Github Personal Access Keys 59 | 60 | Github limits the number of requests per day for unauthenticated clients. If you would like to get 61 | around this limitation, you can configure Cuppa to use a Personal Access Key (PAK) by following the 62 | instructions [here](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-token). You do **not** need to enable any OAuth Scopes. 63 | 64 | Example: 65 | ``` toml 66 | [github] 67 | key = "" 68 | ``` 69 | 70 | ## Usage 71 | 72 | All `cuppa` commands follow the format: 73 | 74 | `cuppa CMD URL` 75 | 76 | where CMD is the action to perform and URL is the link to an existing source. 77 | 78 | ### Commands (CMD) 79 | 80 | | CMD | Alias | Description | 81 | | -------- | ----- | -------------------------------------------------- | 82 | | help | ? | Get help for the other commands. | 83 | | latest | l | Get the details for the latest (non-beta) release. | 84 | | quick | q | Get just the new version number and URL if found. | 85 | | releases | r | Get all known previous (non-beta) releases. | 86 | 87 | ### Example Sources 88 | 89 | | Provider | URL | 90 | | ---------- | --- | 91 | | CPAN | https://cpan.metacpan.org/authors/id/T/TO/TODDR/IO-1.39.tar.gz | 92 | | Git | https://github.com/DataDrake/cuppa.git | 93 | | Github | https://github.com/DataDrake/cuppa/archive/v1.0.4.tar.gz | 94 | | GitLab | https://gitlab.com/corectrl/corectrl/-/archive/v1.0.6/corectrl-v1.0.6.tar.gz | 95 | | GNOME | https://download.gnome.org/sources/gnome-music/3.28/gnome-music-3.28.2.tar.xz | 96 | | Hackage | https://hackage.haskell.org/package/mtl-2.2.2/mtl-2.2.2.tar.gz | 97 | | HTML | http://telepathy.freedesktop.org/releases/telepathy-logger/telepathy-logger-0.8.2.tar.bz2 | 98 | | JetBrains | https://download.jetbrains.com/ruby/RubyMine-2017.3.3.tar.gz | 99 | | KDE | https://download.kde.org/stable/applications/18.12.0/src/akonadi-18.12.0.tar.xz | 100 | | Launchpad | https://launchpad.net/catfish-search/1.4/1.4.4/+download/catfish-1.4.4.tar.gz | 101 | | PyPi | https://pypi.python.org/packages/2c/a9/69f67f6d5d2fd80ef3d60dc5bef4971d837dc741be0d53295d3aabb5ec7f/pyparted-3.10.7.tar.gz | 102 | | Rubygems | https://rubygems.org/downloads/sass-3.4.25.gem | 103 | | Soureforge | https://sourceforge.net/projects/libmtp/files/libmtp/1.1.17/libmtp-1.1.17.tar.gz/download | 104 | ## License 105 | 106 | Copyright 2016-2021 Bryan T. Meyers 107 | 108 | Licensed under the Apache License, Version 2.0 (the "License"); 109 | you may not use this file except in compliance with the License. 110 | You may obtain a copy of the License at 111 | 112 | http://www.apache.org/licenses/LICENSE-2.0 113 | 114 | Unless required by applicable law or agreed to in writing, software 115 | distributed under the License is distributed on an "AS IS" BASIS, 116 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 117 | See the License for the specific language governing permissions and 118 | limitations under the License. 119 | 120 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | 4 | # BACKLOG 5 | 6 | - [ ] Allow Github provider to gracefully fail without API token 7 | - [ ] Add Bitbucket provider 8 | - [ ] Improve SourceForge matching 9 | 10 | # COMPLETED 11 | 12 | - [x] Underline in waterlog printouts 13 | - [x] Upgrade to cli-ng v2 14 | - [x] Code clean-up 15 | - [x] Update copyright notices 16 | 17 | -------------------------------------------------------------------------------- /cli/latest.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package cli 18 | 19 | import ( 20 | "github.com/DataDrake/cli-ng/v2/cmd" 21 | "github.com/DataDrake/cuppa/providers" 22 | log "github.com/DataDrake/waterlog" 23 | ) 24 | 25 | func init() { 26 | cmd.Register(&Latest) 27 | } 28 | 29 | // Latest gets the most recent release for a given source 30 | var Latest = cmd.Sub{ 31 | Name: "latest", 32 | Alias: "l", 33 | Short: "Get the latest stable release", 34 | Args: &LatestArgs{}, 35 | Run: LatestRun, 36 | } 37 | 38 | // LatestArgs contains the arguments for the "latest" subcommand 39 | type LatestArgs struct { 40 | URL string `desc:"Location of a previous source archive"` 41 | } 42 | 43 | // LatestRun carries out finding the latest release 44 | func LatestRun(r *cmd.Root, c *cmd.Sub) { 45 | args := c.Args.(*LatestArgs) 46 | found := false 47 | for _, p := range providers.All() { 48 | log.Infof("\033[1m%s\033[22m checking for match:\n", p) 49 | match := p.Match(args.URL) 50 | if len(match) == 0 { 51 | log.Warnf("\033[1m%s\033[22m does not match.\n", p) 52 | continue 53 | } 54 | r, err := p.Latest(match) 55 | if err != nil { 56 | log.Warnf("Could not get latest \033[1m%s\033[22m, reason: %s\n", match[0], err) 57 | continue 58 | } 59 | found = true 60 | r.Print() 61 | log.Goodf("\033[1m%s\033[22m match(es) found.\n", p) 62 | } 63 | if !found { 64 | log.Fatalln("No release found.") 65 | } 66 | log.Goodln("Done") 67 | } 68 | -------------------------------------------------------------------------------- /cli/quick.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package cli 18 | 19 | import ( 20 | "github.com/DataDrake/cli-ng/v2/cmd" 21 | "github.com/DataDrake/cuppa/providers" 22 | log "github.com/DataDrake/waterlog" 23 | "github.com/DataDrake/waterlog/format" 24 | ) 25 | 26 | func init() { 27 | cmd.Register(&Quick) 28 | } 29 | 30 | // Quick gets the most recent release for a given source, without pretty printing 31 | var Quick = cmd.Sub{ 32 | Name: "quick", 33 | Alias: "q", 34 | Short: "Get the version and location of the most recent release", 35 | Args: &QuickArgs{}, 36 | Run: QuickRun, 37 | } 38 | 39 | // QuickArgs contains the arguments for the "quick" subcommand 40 | type QuickArgs struct { 41 | URL string `desc:"Location of a previous source archive"` 42 | } 43 | 44 | // QuickRun carries out finding the latest release 45 | func QuickRun(r *cmd.Root, c *cmd.Sub) { 46 | args := c.Args.(*QuickArgs) 47 | found := false 48 | log.SetFormat(format.Un) 49 | for _, p := range providers.All() { 50 | match := p.Match(args.URL) 51 | if len(match) == 0 { 52 | continue 53 | } 54 | r, err := p.Latest(match) 55 | if err != nil { 56 | continue 57 | } 58 | found = true 59 | r.PrintSimple() 60 | break 61 | } 62 | if !found { 63 | log.Fatalln("No release found.") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cli/releases.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package cli 18 | 19 | import ( 20 | "github.com/DataDrake/cli-ng/v2/cmd" 21 | "github.com/DataDrake/cuppa/providers" 22 | log "github.com/DataDrake/waterlog" 23 | ) 24 | 25 | func init() { 26 | cmd.Register(&Releases) 27 | } 28 | 29 | // Releases gets all releases for a given source 30 | var Releases = cmd.Sub{ 31 | Name: "releases", 32 | Alias: "r", 33 | Short: "Get all stable releases", 34 | Args: &ReleasesArgs{}, 35 | Run: ReleasesRun, 36 | } 37 | 38 | // ReleasesArgs contains the arguments for the "releases" subcommand 39 | type ReleasesArgs struct { 40 | URL string `desc:"Location of a previous source archive"` 41 | } 42 | 43 | // ReleasesRun carries out finding all releases 44 | func ReleasesRun(r *cmd.Root, c *cmd.Sub) { 45 | args := c.Args.(*ReleasesArgs) 46 | found := false 47 | for _, p := range providers.All() { 48 | log.Infof("\033[1m%s\033[22m checking for match:\n", p) 49 | match := p.Match(args.URL) 50 | if len(match) == 0 { 51 | log.Warnf("\033[1m%s\033[22m does not match.\n", p) 52 | continue 53 | } 54 | rs, err := p.Releases(match) 55 | if err != nil { 56 | log.Warnf("Could not get latest \033[1m%s\033[22m, reason: %s\n", match[0], err) 57 | continue 58 | } 59 | found = true 60 | rs.PrintAll() 61 | log.Goodf("\033[1m%s\033[22m match(es) found.\n", p) 62 | } 63 | if !found { 64 | log.Fatalln("No release found.") 65 | } 66 | log.Goodln("Done") 67 | } 68 | -------------------------------------------------------------------------------- /cli/root.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package cli 18 | 19 | import ( 20 | "github.com/DataDrake/cli-ng/v2/cmd" 21 | log "github.com/DataDrake/waterlog" 22 | "github.com/DataDrake/waterlog/format" 23 | "github.com/DataDrake/waterlog/level" 24 | log2 "log" 25 | ) 26 | 27 | // Root is the main command for this application 28 | var Root = &cmd.Root{ 29 | Name: "cuppa", 30 | Short: "Comprehensive Upstream Provider Polling Assistant", 31 | } 32 | 33 | func init() { 34 | cmd.Register(&cmd.Help) 35 | 36 | //Set up logging 37 | log.SetFlags(log2.Ltime) 38 | log.SetLevel(level.Info) 39 | log.SetFormat(format.Min) 40 | } 41 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package config 18 | 19 | import ( 20 | "github.com/BurntSushi/toml" 21 | log "github.com/DataDrake/waterlog" 22 | "os" 23 | "os/user" 24 | "path/filepath" 25 | ) 26 | 27 | // Config is the configuration for cuppa 28 | type Config struct { 29 | Github struct { 30 | Key string `toml:"key"` 31 | } `toml:"github"` 32 | } 33 | 34 | // Global is the config for all of cuppa at runtime 35 | var Global Config 36 | 37 | // init loads the config if it exists 38 | func init() { 39 | user, err := user.Current() 40 | if err != nil { 41 | return 42 | } 43 | configPath := filepath.Join(user.HomeDir, ".config", "cuppa") 44 | if _, err = os.Stat(configPath); err != nil { 45 | if os.IsNotExist(err) { 46 | return 47 | } 48 | log.Fatalf("Failed to stat config, reason: '%s'\n", err) 49 | } 50 | if _, err = toml.DecodeFile(configPath, &Global); err != nil { 51 | log.Fatalf("Failed to read config, reason: '%s'\n", err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cuppa.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/cli" 21 | ) 22 | 23 | func main() { 24 | cli.Root.Run() 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DataDrake/cuppa 2 | 3 | require ( 4 | github.com/BurntSushi/toml v0.3.0 5 | github.com/DataDrake/cli-ng/v2 v2.0.2 6 | github.com/DataDrake/waterlog v1.0.5 7 | github.com/davecgh/go-spew v1.1.1 // indirect 8 | github.com/jlaffaye/ftp v0.0.0-20181101112434-47f21d10f0ee 9 | github.com/stretchr/testify v1.6.1 // indirect 10 | ) 11 | 12 | go 1.13 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= 2 | github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/DataDrake/cli-ng/v2 v2.0.2 h1:7+25l25VmlERCE95glW6QKBUF13vxqAM2jasFiN02xQ= 4 | github.com/DataDrake/cli-ng/v2 v2.0.2/go.mod h1:bU9YaNNWWVq0eIdDsU3TCe9+7Jb398iBBoqee5EiKWQ= 5 | github.com/DataDrake/waterlog v1.0.5 h1:+c506dboTQh4MoHHwdVtNa9E8K/3qAM/lieke0mH/mE= 6 | github.com/DataDrake/waterlog v1.0.5/go.mod h1:LUv2H3zT/FSN3SNoK/acxHKEaRjI6d+Sio85BfxNOG8= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 11 | github.com/jlaffaye/ftp v0.0.0-20181101112434-47f21d10f0ee h1:oCvgfeGIc6GipidJVyG0Hd9R/w6TO8bBYyJg15ZgJkw= 12 | github.com/jlaffaye/ftp v0.0.0-20181101112434-47f21d10f0ee/go.mod h1:lli8NYPQOFy3O++YmYbqVgOcQ1JPCwdOy+5zSjKJ9qY= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 17 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 19 | golang.org/x/tools v0.0.0-20181221235234-d00ac6d27372/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /providers/cpan/module.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package cpan 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/util" 22 | ) 23 | 24 | // APIModule holds the name of a Perl Module 25 | type APIModule struct { 26 | Module string `json:"main_module"` 27 | } 28 | 29 | // APIRelease is the format string for the metacpan release API 30 | const APIRelease = "https://fastapi.metacpan.org/v1/release/%s" 31 | 32 | func nameToModule(name string) (module string, err error) { 33 | url := fmt.Sprintf(APIRelease, name) 34 | var r APIModule 35 | if err = util.FetchJSON(url, "CPAN module", &r); err == nil { 36 | module = r.Module 37 | } 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /providers/cpan/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package cpan 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "github.com/DataDrake/cuppa/util" 23 | "regexp" 24 | "strings" 25 | ) 26 | 27 | // APIDownloadURL is the format string for the metacpan download_url API 28 | const APIDownloadURL = "https://fastapi.metacpan.org/v1/download_url/%s" 29 | 30 | // SearchRegex is the regexp for "search.cpan.org" 31 | var SearchRegex = regexp.MustCompile("https?://*(?:/.*cpan.org)(?:/CPAN)?/authors/id/(.+)$") 32 | 33 | // Provider is the upstream provider interface for CPAN 34 | type Provider struct{} 35 | 36 | // String gives the name of this provider 37 | func (c Provider) String() string { 38 | return "CPAN" 39 | } 40 | 41 | // Match checks to see if this provider can handle this kind of query 42 | func (c Provider) Match(query string) (params []string) { 43 | if sm := SearchRegex.FindStringSubmatch(query); len(sm) > 0 { 44 | sms := strings.Split(sm[1], "/") 45 | filename := sms[len(sms)-1] 46 | pieces := strings.Split(filename, "-") 47 | if len(pieces) > 2 { 48 | params = append(params, strings.Join(pieces[0:len(pieces)-1], "-")) 49 | return 50 | } 51 | params = append(params, pieces[0]) 52 | } 53 | return 54 | } 55 | 56 | // Latest finds the newest release for a CPAN package 57 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 58 | name := params[0] 59 | module, err := nameToModule(name) 60 | if err != nil { 61 | return 62 | } 63 | url := fmt.Sprintf(APIDownloadURL, module) 64 | var rel Release 65 | if err = util.FetchJSON(url, "latest", &rel); err != nil { 66 | return 67 | } 68 | if len(rel.Error) > 0 { 69 | err = results.NotFound 70 | return 71 | } 72 | if r = rel.Convert(name); r == nil { 73 | err = results.NotFound 74 | } 75 | return 76 | } 77 | 78 | // Releases finds all matching releases for a CPAN package 79 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 80 | if r, err := c.Latest(params); err == nil { 81 | rs = results.NewResultSet(params[0]) 82 | rs.AddResult(r) 83 | } 84 | return 85 | } 86 | -------------------------------------------------------------------------------- /providers/cpan/release.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package cpan 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | "time" 22 | ) 23 | 24 | // Release is a JSON representation of a CPAN release 25 | type Release struct { 26 | Version string `json:"version"` 27 | Status string `json:"status"` 28 | Date string `json:"date"` 29 | Location string `json:"download_url"` 30 | Error string `json:"error"` 31 | } 32 | 33 | // Convert turns a CPAN release into a Cuppa result 34 | func (cr *Release) Convert(name string) *results.Result { 35 | if cr.Status != "latest" { 36 | return nil 37 | } 38 | t, _ := time.Parse(time.RFC3339, cr.Date) 39 | r := results.NewResult(name, cr.Version, cr.Location, t) 40 | if r.Version[0] == "N/A" { 41 | return nil 42 | } 43 | return r 44 | } 45 | -------------------------------------------------------------------------------- /providers/cpan/releases.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package cpan 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | ) 22 | 23 | // Releases is a collection of CPAN releases 24 | type Releases struct { 25 | Releases []Release `json:"releases"` 26 | } 27 | 28 | // Convert turns Releases into a Cuppa ResultSet 29 | func (crs *Releases) Convert(name string) *results.ResultSet { 30 | rs := results.NewResultSet(name) 31 | for _, rel := range crs.Releases { 32 | if r := rel.Convert(name); r != nil { 33 | rs.AddResult(r) 34 | } 35 | } 36 | return rs 37 | } 38 | -------------------------------------------------------------------------------- /providers/git/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package git 18 | 19 | import ( 20 | "bufio" 21 | "bytes" 22 | "github.com/DataDrake/cuppa/results" 23 | "io" 24 | "os" 25 | "os/exec" 26 | "strings" 27 | "time" 28 | ) 29 | 30 | // Provider provides a common interface for each of the backend providers 31 | type Provider struct{} 32 | 33 | // String returns the name of this provider 34 | func (p Provider) String() string { 35 | return "Git" 36 | } 37 | 38 | // Match checks to see if this provider can handle this kind of query 39 | func (p Provider) Match(query string) (params []string) { 40 | if strings.HasPrefix(query, "git|") || strings.HasSuffix(query, ".git") { 41 | params = append(params, strings.TrimPrefix(query, "git|")) 42 | } 43 | return 44 | } 45 | 46 | // Latest finds the newest release for a Git package 47 | func (p Provider) Latest(params []string) (r *results.Result, err error) { 48 | rs, err := p.Releases(params) 49 | if err == nil { 50 | r = rs.Last() 51 | } 52 | return 53 | } 54 | 55 | // Releases finds all matching releases for a Git package 56 | func (p Provider) Releases(params []string) (rs *results.ResultSet, err error) { 57 | name := params[0] 58 | pieces := strings.Split(name, "/") 59 | repoName := strings.TrimSuffix(pieces[len(pieces)-1], ".git") 60 | tmp := "/tmp/" + repoName 61 | defer os.RemoveAll(tmp) 62 | // Shallow clone repo to temp directory 63 | cmd := exec.Command("git", "clone", "--depth=1", name) 64 | cmd.Dir = "/tmp" 65 | if err := cmd.Run(); err == nil { 66 | // Fetch tags from remote 67 | cmd = exec.Command("git", "fetch", "--tags", "--depth=1") 68 | cmd.Dir = tmp 69 | err = cmd.Run() 70 | } 71 | if err != nil { 72 | err = results.Unavailable 73 | return 74 | } 75 | // Read git tags 76 | var buff bytes.Buffer 77 | read := bufio.NewReader(&buff) 78 | cmd = exec.Command("git", "log", "--tags", "-n 10", "--format='%S %cI'") 79 | cmd.Dir = tmp 80 | cmd.Stdout = &buff 81 | cmd.Run() 82 | // Convert tags to releases 83 | rs = results.NewResultSet(name) 84 | line, _, err := read.ReadLine() 85 | for err == nil { 86 | pieces = strings.Fields(string(line)) 87 | tag := pieces[0] 88 | date, _ := time.Parse("2006-01-02T15:04:05-07:00", pieces[1]) 89 | r := results.NewResult(repoName, tag, "git|"+name, date) 90 | rs.AddResult(r) 91 | line, _, err = read.ReadLine() 92 | } 93 | if err != io.EOF || rs.Len() == 0 { 94 | err = results.NotFound 95 | return 96 | } 97 | err = nil 98 | return 99 | } 100 | -------------------------------------------------------------------------------- /providers/github/graphql.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package github 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "github.com/DataDrake/cuppa/config" 24 | "github.com/DataDrake/cuppa/results" 25 | log "github.com/DataDrake/waterlog" 26 | "net/http" 27 | "strings" 28 | "time" 29 | ) 30 | 31 | // GraphQLAPI is the location of the GraphQL Endpoint 32 | const GraphQLAPI = "https://api.github.com/graphql" 33 | 34 | // RepoQueryFormat is the text format for the necessary GraphQL query 35 | const RepoQueryFormat = ` 36 | query { 37 | repository(owner: "%s", name: "%s") { 38 | releases (last: %d) { 39 | nodes { 40 | name 41 | publishedAt 42 | isPrerelease 43 | tag { 44 | name 45 | } 46 | } 47 | } 48 | refs (refPrefix: "refs/tags/", last: %d){ 49 | nodes { 50 | name 51 | target { 52 | ... on Commit { 53 | committedDate 54 | } 55 | ... on Tag { 56 | tagger { 57 | date 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | ` 66 | 67 | // RepoQuery is the JSON payload for this request 68 | type RepoQuery struct { 69 | Query string `json:"query"` 70 | } 71 | 72 | // RepoQueryResult is the JSON payload of the response 73 | type RepoQueryResult struct { 74 | Data struct { 75 | Repository struct { 76 | Releases struct { 77 | Nodes []struct { 78 | Name string `json:"name"` 79 | PublishedAt string `json:"publishedAt"` 80 | IsPrerelease bool `json:"isPrerelease"` 81 | Tag struct { 82 | Name string `json:"name"` 83 | } `json:"tag"` 84 | } `json:"nodes"` 85 | } `json:"releases"` 86 | Refs struct { 87 | Nodes []struct { 88 | Name string `json:"name"` 89 | Target struct { 90 | Date string `json:"committedDate"` 91 | Tagger struct { 92 | Date string `json:"date"` 93 | } `json:"tagger"` 94 | } `json:"target"` 95 | } `json:"nodes"` 96 | } `json:"refs"` 97 | } `json:"repository"` 98 | } `json:"data"` 99 | } 100 | 101 | // Convert turns a RepoQueryResult into a Cuppa ResultSet 102 | func (rqr RepoQueryResult) Convert(name string) (rs *results.ResultSet) { 103 | rs = results.NewResultSet(name) 104 | var err error 105 | for _, tag := range rqr.Data.Repository.Refs.Nodes { 106 | pre := false 107 | found := false 108 | var published time.Time 109 | for _, node := range rqr.Data.Repository.Releases.Nodes { 110 | if node.Tag.Name == tag.Name { 111 | found = true 112 | if node.IsPrerelease { 113 | pre = true 114 | } 115 | published, _ = time.Parse(time.RFC3339, node.PublishedAt) 116 | } 117 | } 118 | if pre { 119 | continue 120 | } 121 | if !found { 122 | if len(tag.Target.Date) > 0 { 123 | published, _ = time.Parse(time.RFC3339, tag.Target.Date) 124 | } else if len(tag.Target.Tagger.Date) > 0 { 125 | published, err = time.Parse(time.RFC3339, tag.Target.Tagger.Date) 126 | if err != nil { 127 | published, _ = time.Parse("2006-01-02T15:04:05-07:00", tag.Target.Tagger.Date) 128 | } 129 | } 130 | } 131 | r := results.NewResult(name, tag.Name, fmt.Sprintf(SourceFormat, name, tag.Name), published) 132 | rs.AddResult(r) 133 | } 134 | return 135 | } 136 | 137 | // GetReleases gets a number of releases for a given repo 138 | func (c Provider) GetReleases(name string, max int) (rs *results.ResultSet, err error) { 139 | names := strings.Split(name, "/") 140 | query := RepoQuery{ 141 | Query: fmt.Sprintf(RepoQueryFormat, names[0], names[1], max, max), 142 | } 143 | var buff bytes.Buffer 144 | enc := json.NewEncoder(&buff) 145 | if err = enc.Encode(&query); err != nil { 146 | log.Debugf("Failed to encode request: %s\n", err) 147 | err = results.Unavailable 148 | return 149 | } 150 | req, _ := http.NewRequest("POST", GraphQLAPI, &buff) 151 | if key := config.Global.Github.Key; len(key) > 0 { 152 | req.Header["Authorization"] = []string{"token " + key} 153 | } 154 | resp, err := http.DefaultClient.Do(req) 155 | if err != nil { 156 | log.Debugf("Failed to get releases: %s\n", err) 157 | err = results.Unavailable 158 | return 159 | } 160 | defer resp.Body.Close() 161 | // Translate Status Code 162 | switch resp.StatusCode { 163 | case 200: 164 | break 165 | case 404: 166 | err = results.NotFound 167 | return 168 | default: 169 | err = results.Unavailable 170 | return 171 | } 172 | // Decode response 173 | dec := json.NewDecoder(resp.Body) 174 | var rqr RepoQueryResult 175 | if err = dec.Decode(&rqr); err != nil { 176 | log.Debugf("Failed to decode response: %s\n", err) 177 | err = results.Unavailable 178 | return 179 | } 180 | rs = rqr.Convert(name) 181 | return 182 | } 183 | -------------------------------------------------------------------------------- /providers/github/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package github 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | "regexp" 22 | ) 23 | 24 | const ( 25 | // SourceFormat is the format string for Github release tarballs 26 | SourceFormat = "https://github.com/%s/archive/%s.tar.gz" 27 | ) 28 | 29 | var ( 30 | // SourceRegex is the regex for Github sources 31 | SourceRegex = regexp.MustCompile("github.com/([^/]+/[^/.]+)") 32 | // VersionRegex is used to parse Github version numbers 33 | VersionRegex = regexp.MustCompile("(?:\\d+\\.)*\\d+\\w*") 34 | ) 35 | 36 | // Provider is the upstream provider interface for github 37 | type Provider struct{} 38 | 39 | // String gives the name of this provider 40 | func (c Provider) String() string { 41 | return "GitHub" 42 | } 43 | 44 | // Match checks to see if this provider can handle this kind of query 45 | func (c Provider) Match(query string) (params []string) { 46 | if sm := SourceRegex.FindStringSubmatch(query); len(sm) > 1 { 47 | params = sm[1:] 48 | } 49 | return 50 | } 51 | 52 | // Latest finds the newest release for a github package 53 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 54 | rs, err := c.GetReleases(params[0], 100) 55 | if err == nil { 56 | r = rs.Last() 57 | } 58 | return 59 | } 60 | 61 | // Releases finds all matching releases for a github package 62 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 63 | rs, err = c.GetReleases(params[0], 100) 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /providers/gitlab/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package gitlab 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "github.com/DataDrake/cuppa/util" 23 | "regexp" 24 | "strings" 25 | ) 26 | 27 | const ( 28 | // SourceFormat is the format string for GitLab release tarballs 29 | SourceFormat = "https://%s/%s/-/archive/%s/%s.tar.gz" 30 | // TagsEndpoint is the API endpoint URL for GitLab project tags 31 | TagsEndpoint = "https://%s/api/v4/projects/%s/repository/tags" 32 | ) 33 | 34 | var ( 35 | // SourceRegex is the regex for GitLab sources 36 | SourceRegex = regexp.MustCompile("https?://(gitlab[^/]+)/(.+/[^/.]+)/\\-/") 37 | // VersionRegex is used to parse GitLab version numbers 38 | VersionRegex = regexp.MustCompile("(?:\\d+\\.)*\\d+\\w*") 39 | ) 40 | 41 | // Provider is the upstream provider interface for GitLab 42 | type Provider struct{} 43 | 44 | // String gives the name of this provider 45 | func (c Provider) String() string { 46 | return "GitLab" 47 | } 48 | 49 | // Match checks to see if this provider can handle this kind of query 50 | func (c Provider) Match(query string) (params []string) { 51 | if sm := SourceRegex.FindStringSubmatch(query); len(sm) > 2 { 52 | params = sm[1:] 53 | } 54 | return 55 | } 56 | 57 | // Latest finds the newest release for a GitLab package 58 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 59 | rs, err := c.Releases(params) 60 | if err == nil { 61 | r = rs.Last() 62 | } 63 | return 64 | } 65 | 66 | // Releases finds all matching releases for a GitLab package 67 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 68 | // Query the API 69 | id := strings.Join(strings.Split(params[1], "/"), "%2f") 70 | url := fmt.Sprintf(TagsEndpoint, params[0], id) 71 | var tags Tags 72 | if err = util.FetchJSON(url, "releases", &tags); err != nil { 73 | return 74 | } 75 | rs = tags.Convert(params[0], params[1]) 76 | return 77 | } 78 | -------------------------------------------------------------------------------- /providers/gitlab/tag.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package gitlab 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | "time" 23 | 24 | "github.com/DataDrake/cuppa/results" 25 | ) 26 | 27 | // Commit is a JSON representation of a GitLab commit 28 | type Commit struct { 29 | AuthoredDate string `json:"authored_date"` 30 | } 31 | 32 | // Release is a JSON representation of a GitLab tag release 33 | type Release struct { 34 | TagName string `json:"tag_name"` 35 | } 36 | 37 | // Tag is a JSON representation of a GitLab tag 38 | type Tag struct { 39 | Name string `json:"name"` 40 | Commit Commit `json:"commit"` 41 | Release Release `json:"release"` 42 | } 43 | 44 | // Convert turns a GitLab tag into a Cuppa result 45 | func (gl Tag) Convert(host, name string) *results.Result { 46 | published, _ := time.Parse(time.RFC3339, gl.Commit.AuthoredDate) 47 | file := fmt.Sprintf("%s-%s", strings.Split(name, "/")[1], gl.Name) 48 | loc := fmt.Sprintf(SourceFormat, host, name, gl.Name, file) 49 | version := gl.Release.TagName 50 | if len(version) == 0 { 51 | vs := strings.Split(gl.Name, "-") 52 | version = vs[len(vs)-1] 53 | } 54 | return results.NewResult(name, version, loc, published) 55 | } 56 | -------------------------------------------------------------------------------- /providers/gitlab/tags.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package gitlab 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | ) 22 | 23 | // Tags is a set of one or more GitLab tags 24 | type Tags []Tag 25 | 26 | // Convert turns a GitLab result set into a Cuppa ResultSet 27 | func (gls Tags) Convert(host, name string) *results.ResultSet { 28 | rs := results.NewResultSet(name) 29 | for _, rel := range gls { 30 | r := rel.Convert(host, name) 31 | rs.AddResult(r) 32 | } 33 | return rs 34 | } 35 | -------------------------------------------------------------------------------- /providers/gnome/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package gnome 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "github.com/DataDrake/cuppa/results" 23 | log "github.com/DataDrake/waterlog" 24 | "net/http" 25 | "regexp" 26 | "strconv" 27 | "strings" 28 | "time" 29 | ) 30 | 31 | const ( 32 | // CacheAPI is the string format for GNOME cache.json files 33 | CacheAPI = "https://download.gnome.org/sources/%s/cache.json" 34 | // SourceFormat is the string format for GNOME sources 35 | SourceFormat = "https://download.gnome.org/sources/%s/%s" 36 | ) 37 | 38 | // TarballRegex matches GNOME sources 39 | var TarballRegex = regexp.MustCompile("https?://(?:ftp.gnome.org/pub/gnome|download.gnome.org)/sources/(.+?)/.*") 40 | 41 | // Provider is the upstream provider interface for GNOME 42 | type Provider struct{} 43 | 44 | // String gives the name of this provider 45 | func (c Provider) String() string { 46 | return "GNOME" 47 | } 48 | 49 | // Match checks to see if this provider can handle this kind of query 50 | func (c Provider) Match(query string) (params []string) { 51 | if sm := TarballRegex.FindStringSubmatch(query); len(sm) > 1 { 52 | params = sm[1:] 53 | } 54 | return 55 | } 56 | 57 | // Latest finds the newest release for a GNOME package 58 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 59 | rs, err := c.Releases(params) 60 | if err == nil { 61 | r = rs.Last() 62 | } 63 | return 64 | } 65 | 66 | // Releases finds all matching releases for a rubygems package 67 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 68 | name := params[0] 69 | // Query the API 70 | resp, err := http.Get(fmt.Sprintf(CacheAPI, name)) 71 | if err != nil { 72 | log.Debugf("Failed to fetch releases: %s\n", err) 73 | err = results.Unavailable 74 | return 75 | } 76 | defer resp.Body.Close() 77 | // Translate Status Code 78 | switch resp.StatusCode { 79 | case 200: 80 | break 81 | case 404: 82 | err = results.NotFound 83 | return 84 | default: 85 | err = results.Unavailable 86 | return 87 | } 88 | // Decode response 89 | dec := json.NewDecoder(resp.Body) 90 | var raw []interface{} 91 | if err = dec.Decode(&raw); err != nil { 92 | log.Debugf("Failed to decode response: %s\n", err) 93 | err = results.Unavailable 94 | return 95 | } 96 | if len(raw) < 3 { 97 | err = results.Unavailable 98 | return 99 | } 100 | rs = Merge(name, raw[1].(map[string]interface{}), raw[2].(map[string]interface{})) 101 | return 102 | } 103 | 104 | // Merge combines Source and Versions into a ResultSet 105 | func Merge(name string, srcs, vs map[string]interface{}) (rs *results.ResultSet) { 106 | rs = results.NewResultSet(name) 107 | if srcs[name] == nil || vs[name] == nil { 108 | return 109 | } 110 | for _, v := range vs[name].([]interface{}) { 111 | pieces := strings.Split(v.(string), ".") 112 | if len(pieces) < 2 { 113 | continue 114 | } 115 | minor, err := strconv.Atoi(pieces[1]) 116 | if err != nil { 117 | continue 118 | } 119 | // Filter out unstable releases 120 | if minor%2 == 1 { 121 | continue 122 | } 123 | files := srcs[name].(map[string]interface{})[v.(string)].(map[string]interface{}) 124 | if len(files) == 0 { 125 | continue 126 | } 127 | // get location of tarball 128 | var location string 129 | switch { 130 | case files["tar.xz"] != nil: 131 | location = fmt.Sprintf(SourceFormat, name, files["tar.xz"].(string)) 132 | case files["tar.gz"] != nil: 133 | location = fmt.Sprintf(SourceFormat, name, files["tar.gz"].(string)) 134 | case files["tar.bz2"] != nil: 135 | location = fmt.Sprintf(SourceFormat, name, files["tar.bz2"].(string)) 136 | default: 137 | continue 138 | } 139 | r := results.NewResult(name, v.(string), location, time.Time{}) 140 | rs.AddResult(r) 141 | } 142 | return 143 | } 144 | -------------------------------------------------------------------------------- /providers/gnu/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package gnu 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | log "github.com/DataDrake/waterlog" 23 | "github.com/jlaffaye/ftp" 24 | "regexp" 25 | "sort" 26 | ) 27 | 28 | const ( 29 | // MirrorsFTP is the host to use as a GNU mirror 30 | MirrorsFTP = "mirrors.rit.edu:21" 31 | // GNUFormat is the format string for GNU sources 32 | GNUFormat = "https://mirrors.rit.edu/gnu/%s/%s" 33 | ) 34 | 35 | var ( 36 | // MirrorsRegex is a regex for a GNU mirror source 37 | MirrorsRegex = regexp.MustCompile("(?:https?|ftp)://[^\\/]+/gnu/(.+)/[^\\/]+$") 38 | // TarballRegex is a regex for finding tarball files 39 | TarballRegex = regexp.MustCompile("^(.+)-(.+)\\.tar\\..+z$") 40 | ) 41 | 42 | // Provider is the upstream provider interface for GNU 43 | type Provider struct{} 44 | 45 | // String gives the name of this provider 46 | func (c Provider) String() string { 47 | return "GNU" 48 | } 49 | 50 | // Match checks to see if this provider can handle this kind of query 51 | func (c Provider) Match(query string) (params []string) { 52 | if sm := MirrorsRegex.FindStringSubmatch(query); len(sm) > 1 { 53 | params = sm[1:] 54 | } 55 | return 56 | } 57 | 58 | // Latest finds the newest release for a GNU package 59 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 60 | rs, err := c.Releases(params) 61 | if err == nil { 62 | r = rs.Last() 63 | } 64 | return 65 | } 66 | 67 | // Releases finds all matching releases for a GNU package 68 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 69 | name := params[0] 70 | client, err := ftp.Dial(MirrorsFTP) 71 | if err != nil { 72 | log.Debugf("Failed to connect to FTP server: %s\n", err) 73 | err = results.Unavailable 74 | return 75 | } 76 | if err = client.Login("anonymous", "anonymous"); err != nil { 77 | log.Debugf("Failed to login to FTP server: %s\n", err) 78 | err = results.Unavailable 79 | return 80 | } 81 | entries, err := client.List("gnu" + "/" + name) 82 | if err != nil { 83 | log.Debugf("FTP Error: %s\n", err.Error()) 84 | err = results.NotFound 85 | return 86 | } 87 | rs = results.NewResultSet(name) 88 | for _, entry := range entries { 89 | if entry.Type != ftp.EntryTypeFile { 90 | continue 91 | } 92 | if sm := TarballRegex.FindStringSubmatch(entry.Name); len(sm) > 2 { 93 | r := results.NewResult(sm[1], sm[2], fmt.Sprintf(GNUFormat, name, entry.Name), entry.Time) 94 | rs.AddResult(r) 95 | } 96 | } 97 | if rs.Len() == 0 { 98 | err = results.NotFound 99 | } 100 | sort.Sort(rs) 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /providers/hackage/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package hackage 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "github.com/DataDrake/cuppa/util" 23 | log "github.com/DataDrake/waterlog" 24 | "io/ioutil" 25 | "net/http" 26 | "regexp" 27 | ) 28 | 29 | const ( 30 | // TarballAPI is the format string for the Hackage Tarball API 31 | TarballAPI = "https://hackage.haskell.org/package/%s-%s/%s-%s.tar.gz" 32 | // UploadTimeAPI is the format string for the Hackage Upload Time API 33 | UploadTimeAPI = "https://hackage.haskell.org/package/%s-%s/upload-time" 34 | // VersionsAPI is the format string for the Hackage Versions API 35 | VersionsAPI = "https://hackage.haskell.org/package/%s/preferred" 36 | ) 37 | 38 | // TarballRegex matches HAckage tarballs 39 | var TarballRegex = regexp.MustCompile("https?://hackage.haskell.org/package/.*/(.*)-(.*?).tar.gz") 40 | 41 | // Provider is the upstream provider interface for hackage 42 | type Provider struct{} 43 | 44 | // String gives the name of this provider 45 | func (c Provider) String() string { 46 | return "Hackage" 47 | } 48 | 49 | // Match checks to see if this provider can handle this kind of query 50 | func (c Provider) Match(query string) (params []string) { 51 | if sm := TarballRegex.FindStringSubmatch(query); len(sm) > 1 { 52 | params = sm[1:] 53 | } 54 | return 55 | } 56 | 57 | // Latest finds the newest release for a hackage package 58 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 59 | rs, err := c.Releases(params) 60 | if err == nil { 61 | r = rs.First() 62 | } 63 | return 64 | } 65 | 66 | // Releases finds all matching releases for a hackage package 67 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 68 | name := params[0] 69 | url := fmt.Sprintf(VersionsAPI, name) 70 | var versions Versions 71 | if err = util.FetchJSON(url, "versions", &versions); err != nil { 72 | return 73 | } 74 | // Process releases 75 | var hrs Releases 76 | for _, v := range versions.Normal { 77 | hr := Release{ 78 | name: name, 79 | version: v, 80 | } 81 | r, err := http.Get(fmt.Sprintf(UploadTimeAPI, name, v)) 82 | if err != nil { 83 | log.Debugf("Failed to get upload time: %s\n", err) 84 | continue 85 | } 86 | defer r.Body.Close() 87 | dateRaw, err := ioutil.ReadAll(r.Body) 88 | if err != nil { 89 | log.Debugf("Failed to read response: %s\n", err) 90 | continue 91 | } 92 | hr.released = string(dateRaw) 93 | hrs.Releases = append(hrs.Releases, hr) 94 | } 95 | if len(hrs.Releases) == 0 { 96 | err = results.NotFound 97 | return 98 | } 99 | rs = hrs.Convert(name) 100 | err = nil 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /providers/hackage/release.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package hackage 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "time" 23 | ) 24 | 25 | // Versions is a JSON representation of Hackage release version numbers 26 | type Versions struct { 27 | Normal []string `json:"normal-version"` 28 | } 29 | 30 | // Release is a local representation of a Hackage release 31 | type Release struct { 32 | name string 33 | released string 34 | version string 35 | } 36 | 37 | // Convert turns a Hackage release into a Cuppa result 38 | func (hr *Release) Convert() *results.Result { 39 | pub, _ := time.Parse(time.UnixDate, hr.released) 40 | loc := fmt.Sprintf(TarballAPI, hr.name, hr.version, hr.name, hr.version) 41 | return results.NewResult(hr.name, hr.version, loc, pub) 42 | } 43 | -------------------------------------------------------------------------------- /providers/hackage/releases.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package hackage 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | ) 22 | 23 | // Releases is a representation of one or more Hackage releases 24 | type Releases struct { 25 | Releases []Release 26 | } 27 | 28 | // Convert turns a Hackage result set into a Cuppa result set 29 | func (hrs *Releases) Convert(name string) *results.ResultSet { 30 | rs := results.NewResultSet(name) 31 | for _, rel := range hrs.Releases { 32 | if r := rel.Convert(); r != nil { 33 | rs.AddResult(r) 34 | } 35 | } 36 | return rs 37 | } 38 | -------------------------------------------------------------------------------- /providers/html/config.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package html 18 | 19 | import ( 20 | "encoding/xml" 21 | "github.com/DataDrake/cuppa/results" 22 | log "github.com/DataDrake/waterlog" 23 | "io" 24 | "regexp" 25 | "strings" 26 | "time" 27 | ) 28 | 29 | // ArchiveRegex matches archive filenames 30 | var ArchiveRegex = regexp.MustCompile("^(.+)-(.*)\\.(?:tar\\.[^.]+|zip)$") 31 | 32 | // DownloadList represents an HTML file listing 33 | type DownloadList struct { 34 | Name xml.Name 35 | Entry []struct { 36 | Columns []struct { 37 | XML string `xml:",innerxml"` 38 | Raw string `xml:",chardata"` 39 | } `xml:"td"` 40 | } `xml:"body>table>tr"` 41 | } 42 | 43 | // LocationConfig is a configuration for the Location field in the listing 44 | type LocationConfig struct { 45 | Index int 46 | XML bool 47 | Pattern *regexp.Regexp 48 | } 49 | 50 | // TimeConfig is a configuration for the Modified field in the listing 51 | type TimeConfig struct { 52 | Index int 53 | Layout string 54 | } 55 | 56 | // Config is a configuration for parsing a listing 57 | type Config struct { 58 | Location LocationConfig 59 | Modified TimeConfig 60 | } 61 | 62 | // Parse reads an HTML file listing and converts is to a ResultSet 63 | func (c Config) Parse(name, path string, in io.Reader) (rs *results.ResultSet, err error) { 64 | dec := xml.NewDecoder(in) 65 | dec.Strict = false 66 | dec.AutoClose = xml.HTMLAutoClose 67 | dec.Entity = xml.HTMLEntity 68 | var list DownloadList 69 | if err = dec.Decode(&list); err != nil { 70 | log.Debugf("Failed to decode download list: %s\n", err) 71 | err = results.Unavailable 72 | return 73 | } 74 | rs = results.NewResultSet(name) 75 | for _, entry := range list.Entry { 76 | if len(entry.Columns) >= c.Location.Index && len(entry.Columns) >= c.Modified.Index { 77 | var loc string 78 | if c.Location.XML { 79 | loc = entry.Columns[c.Location.Index].XML 80 | sm := c.Location.Pattern.FindStringSubmatch(loc) 81 | if len(sm) != 2 { 82 | continue 83 | } 84 | loc = sm[1] 85 | } else { 86 | loc = entry.Columns[c.Location.Index].Raw 87 | } 88 | sm := ArchiveRegex.FindStringSubmatch(loc) 89 | if len(sm) != 3 { 90 | continue 91 | } 92 | n := sm[1] 93 | version := sm[2] 94 | if n != name { 95 | continue 96 | } 97 | mod, e := time.Parse(c.Modified.Layout, strings.TrimSpace(entry.Columns[c.Modified.Index].Raw)) 98 | if e != nil { 99 | continue 100 | } 101 | r := results.NewResult(n, version, path+loc, mod) 102 | rs.AddResult(r) 103 | } 104 | } 105 | err = nil 106 | return 107 | } 108 | -------------------------------------------------------------------------------- /providers/html/configs.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package html 18 | 19 | import ( 20 | "regexp" 21 | ) 22 | 23 | // HTTPDConfig is a configuration for a standard Apache httpd directory listing 24 | var HTTPDConfig = Config{ 25 | Location: LocationConfig{ 26 | Index: 1, 27 | XML: true, 28 | Pattern: regexp.MustCompile(">(.+)<"), 29 | }, 30 | Modified: TimeConfig{ 31 | Index: 2, 32 | Layout: "2006-01-02 15:04", 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /providers/html/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package html 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | log "github.com/DataDrake/waterlog" 22 | "net/http" 23 | ) 24 | 25 | // Provider is the upstream provider interface for HTML 26 | type Provider struct{} 27 | 28 | // String gives the name of this provider 29 | func (c Provider) String() string { 30 | return "HTML" 31 | } 32 | 33 | // Match checks to see if this provider can handle this kind of query 34 | func (c Provider) Match(query string) (params []string) { 35 | for _, upstream := range upstreams { 36 | if name := upstream.Match(query); len(name) > 0 { 37 | params = append(params, name) 38 | } 39 | } 40 | return 41 | } 42 | 43 | // Latest finds the newest release for a GNOME package 44 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 45 | rs, err := c.Releases(params) 46 | if err == nil { 47 | r = rs.Last() 48 | } 49 | return 50 | } 51 | 52 | // Releases finds all matching releases for a rubygems package 53 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 54 | name := params[0] 55 | var upstream Upstream 56 | for i := range upstreams { 57 | if len(upstreams[i].Match(name)) != 0 { 58 | upstream = upstreams[i] 59 | break 60 | } 61 | } 62 | sm := upstream.HostPattern.FindStringSubmatch(name) 63 | resp, err := http.Get(sm[1]) 64 | if err != nil { 65 | log.Debugf("Failed to get releases: %s\n", err) 66 | err = results.Unavailable 67 | return 68 | } 69 | defer resp.Body.Close() 70 | // Translate Status Code 71 | switch resp.StatusCode { 72 | case 200: 73 | break 74 | case 404: 75 | err = results.NotFound 76 | return 77 | default: 78 | err = results.Unavailable 79 | return 80 | } 81 | if rs, err = upstream.Parse(name, resp.Body); err != nil { 82 | err = results.NotFound 83 | } 84 | return 85 | } 86 | -------------------------------------------------------------------------------- /providers/html/upstream.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package html 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | "io" 22 | "regexp" 23 | ) 24 | 25 | // Upstream pairs a Source matching Regex with a Config 26 | type Upstream struct { 27 | Name string 28 | HostPattern *regexp.Regexp 29 | Conf Config 30 | } 31 | 32 | // Match checks if the provided path matches HostPattern 33 | func (u Upstream) Match(path string) string { 34 | if sm := u.HostPattern.FindStringSubmatch(path); len(sm) > 0 { 35 | return sm[0] 36 | } 37 | return "" 38 | } 39 | 40 | // Parse reads a directory listing for a given path and Upstream 41 | func (u Upstream) Parse(name string, in io.Reader) (*results.ResultSet, error) { 42 | sm := u.HostPattern.FindStringSubmatch(name) 43 | return u.Conf.Parse(sm[2], sm[1], in) 44 | } 45 | -------------------------------------------------------------------------------- /providers/html/upstreams.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package html 18 | 19 | import ( 20 | "regexp" 21 | ) 22 | 23 | var upstreams = []Upstream{ 24 | { 25 | Name: "freedesktop", 26 | HostPattern: regexp.MustCompile("^(https?://.*freedesktop.org/.+\\/)([^\\/]+)-.+?$"), 27 | Conf: HTTPDConfig, 28 | }, 29 | { 30 | Name: "xorg", 31 | HostPattern: regexp.MustCompile("^(https?://.*x.org/.+\\/)([^\\/]+)-.+?$"), 32 | Conf: HTTPDConfig, 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /providers/jetbrains/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package jetbrains 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "github.com/DataDrake/cuppa/util" 23 | "regexp" 24 | "strings" 25 | ) 26 | 27 | // ReleaseCodes provides a mapping between JetBrains products and their API codename 28 | var ReleaseCodes = map[string]string{ 29 | "appcode": "AC", 30 | "clion": "CL", 31 | "datagrip": "DG", 32 | "goglang": "GO", 33 | "ideaiu": "IIU", 34 | "ideaic": "IIC", 35 | "phpstorm": "PS", 36 | "pycharm-professional": "PCP", 37 | "pycharm-ce": "PCC", 38 | "pycharm-community": "PCC", 39 | "pycharm-edu": "PCE", 40 | "rider": "RD", 41 | "rubymine": "RM", 42 | "upsource": "US", 43 | "webstorm": "WS", 44 | } 45 | 46 | const ( 47 | // ReleasesAPI is the format string for the JetBrains Releases API 48 | ReleasesAPI = "https://data.services.jetbrains.com/products/releases?code=%s" 49 | // LatestAPI is the format string for the JetBrains Releases API when asking for latest 50 | LatestAPI = "https://data.services.jetbrains.com/products/releases?code=%s&latest=true" 51 | ) 52 | 53 | // SourceRegex matches JetBrains sources 54 | var SourceRegex = regexp.MustCompile("https?://download.jetbrains.com/.+?/(.+?)-\\d.*") 55 | 56 | // Provider is the upstream provider interface for JetBrains 57 | type Provider struct{} 58 | 59 | // String gives the name of this provider 60 | func (c Provider) String() string { 61 | return "JetBrains" 62 | } 63 | 64 | // Match checks to see if this provider can handle this kind of query 65 | func (c Provider) Match(query string) (params []string) { 66 | if sm := SourceRegex.FindStringSubmatch(query); len(sm) > 1 { 67 | params = append(params, strings.ToLower(sm[1])) 68 | } 69 | return 70 | } 71 | 72 | // Latest finds the newest release for a JetBrains package 73 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 74 | rs, err := c.fetchReleases(LatestAPI, "latest", params) 75 | if err != nil { 76 | return 77 | } 78 | r = rs.First() 79 | return 80 | } 81 | 82 | // Releases finds all matching releases for a JetBrains package 83 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 84 | return c.fetchReleases(ReleasesAPI, "releases", params) 85 | } 86 | 87 | func (c Provider) fetchReleases(api, kind string, params []string) (rs *results.ResultSet, err error) { 88 | name := params[0] 89 | // Query the API 90 | code := ReleaseCodes[name] 91 | var jbs Releases 92 | url := fmt.Sprintf(api, code) 93 | if err = util.FetchJSON(url, kind, &jbs); err != nil { 94 | return 95 | } 96 | if jbs[code] == nil || len(jbs[code]) == 0 { 97 | err = results.NotFound 98 | return 99 | } 100 | rs = jbs.Convert(name) 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /providers/jetbrains/release.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package jetbrains 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | "time" 22 | ) 23 | 24 | // Download is a JSON representation of a JetBrains downloadable source 25 | type Download struct { 26 | ChecksumLink string `json:"checksumLink"` 27 | Link string `json:"link"` 28 | Size uint64 `json:"size"` 29 | } 30 | 31 | // Release is a JSON representation of a JetBrains release 32 | type Release struct { 33 | Build string `json:"build"` 34 | Date string `json:"date"` 35 | Downloads map[string]Download `json:"downloads"` 36 | Notes string `json:"notesLink"` 37 | Type string `json:"type"` 38 | MajorVersion string `json:"majorVersion"` 39 | Version string `json:"version"` 40 | } 41 | 42 | // Convert turns a JetBrains release into a Cuppa result 43 | func (jb Release) Convert() *results.Result { 44 | published, _ := time.Parse("2006-01-02", jb.Date) 45 | if d, ok := jb.Downloads["linuxWithoutJDK"]; ok { 46 | return results.NewResult("", jb.Version, d.Link, published) 47 | } 48 | if d, ok := jb.Downloads["linux"]; ok { 49 | return results.NewResult("", jb.Version, d.Link, published) 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /providers/jetbrains/releases.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package jetbrains 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | ) 22 | 23 | // Releases is a collection of JetBrains releases 24 | type Releases map[string][]Release 25 | 26 | // Convert turns JetBrains releases into a Cuppa result set 27 | func (jbs Releases) Convert(name string) *results.ResultSet { 28 | rs := results.NewResultSet(name) 29 | code := ReleaseCodes[name] 30 | for _, rel := range jbs[code] { 31 | if r := rel.Convert(); r != nil { 32 | r.Name = name 33 | rs.AddResult(r) 34 | } 35 | } 36 | return rs 37 | } 38 | -------------------------------------------------------------------------------- /providers/kde/listing.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package kde 18 | 19 | import ( 20 | "compress/bzip2" 21 | log "github.com/DataDrake/waterlog" 22 | "io/ioutil" 23 | "net/http" 24 | ) 25 | 26 | const ListingURL = "https://download.kde.org/ls-lR.bz2" 27 | 28 | var listing []byte 29 | 30 | func getListing() { 31 | // Query the API 32 | resp, err := http.Get(ListingURL) 33 | if err != nil { 34 | log.Debugf("Failed to get listing: %s\n", err) 35 | return 36 | } 37 | defer resp.Body.Close() 38 | // Translate Status Code 39 | if resp.StatusCode != 200 { 40 | return 41 | } 42 | body := bzip2.NewReader(resp.Body) 43 | listing, _ = ioutil.ReadAll(body) 44 | } 45 | -------------------------------------------------------------------------------- /providers/kde/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package kde 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "github.com/DataDrake/cuppa/results" 23 | "regexp" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | const ( 29 | // ListingPrefix is the prefix of all paths in the KDE listing that is hidden by HTTP 30 | ListingPrefix = "/srv/archives/ftp/" 31 | // SourceFormat3 is the string format for KDE sources with 3 pieces 32 | SourceFormat3 = "https://download.kde.org/%s/%s/%s-%s.tar.xz" 33 | // SourceFormat4 is the string format for KDE sources with 4 pieces 34 | SourceFormat4 = "https://download.kde.org/%s/%s/%s/%s-%s.tar.xz" 35 | // SourceFormat5 is the string format for KDE sources with 5 pieces 36 | SourceFormat5 = "https://download.kde.org/%s/%s/%s/%s/%s-%s.tar.xz" 37 | // SourceFormat6 is the string format for KDE sources with 6 pieces 38 | SourceFormat6 = "https://download.kde.org/%s/%s/%s/%s/%s/%s-%s.tar.xz" 39 | ) 40 | 41 | // TarballRegex matches KDE sources 42 | var TarballRegex = regexp.MustCompile("https?://.*download.kde.org/(.+)") 43 | 44 | // Provider is the upstream provider interface for KDE 45 | type Provider struct{} 46 | 47 | // String gives the name of this provider 48 | func (c Provider) String() string { 49 | return "KDE" 50 | } 51 | 52 | // Match checks to see if this provider can handle this kind of query 53 | func (c Provider) Match(query string) (params []string) { 54 | if sm := TarballRegex.FindStringSubmatch(query); len(sm) > 1 { 55 | pieces := strings.Split(sm[1], "/") 56 | if len(pieces) > 2 || len(pieces) < 7 { 57 | params = append(params, sm[1]) 58 | } 59 | } 60 | return 61 | } 62 | 63 | // Latest finds the newest release for a KDE package 64 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 65 | rs, err := c.Releases(params) 66 | if err == nil { 67 | r = rs.Last() 68 | } 69 | return 70 | } 71 | 72 | // Releases finds all matching releases for a KDE package 73 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 74 | name := params[0] 75 | if len(listing) == 0 { 76 | getListing() 77 | } 78 | buff := bytes.NewBuffer(listing) 79 | pieces := strings.Split(name, "/") 80 | pieces2 := strings.Split(pieces[len(pieces)-1], "-") 81 | name = strings.Join(pieces2[0:len(pieces2)-1], "-") 82 | var searchPrefix string 83 | switch len(pieces) { 84 | case 3: 85 | searchPrefix = ListingPrefix + strings.Join(pieces[0:len(pieces)-1], "/") + ":\n" 86 | case 4: 87 | searchPrefix = ListingPrefix + strings.Join(pieces[0:len(pieces)-2], "/") + ":\n" 88 | case 5, 6: 89 | searchPrefix = ListingPrefix + strings.Join(pieces[0:len(pieces)-3], "/") + ":\n" 90 | } 91 | rs = results.NewResultSet(name) 92 | for { 93 | line, err := buff.ReadString('\n') 94 | if err != nil { 95 | break 96 | } 97 | if line != searchPrefix { 98 | continue 99 | } 100 | for line != "\n" { 101 | line, err = buff.ReadString('\n') 102 | if err != nil || line == "\n" { 103 | break 104 | } 105 | fields := strings.Fields(line) 106 | fd := fields[len(fields)-1] 107 | parts := strings.Split(fd, "-") 108 | last := parts[len(parts)-1] 109 | parts = strings.Split(last, ".") 110 | var vRaw []string 111 | for _, p := range parts { 112 | if p[0] > 57 || p[0] < 48 { 113 | break 114 | } 115 | vRaw = append(vRaw, p) 116 | } 117 | version := strings.Join(vRaw, ".") 118 | if len(version) == 0 || version[0] > 57 || version[0] < 48 { 119 | continue 120 | } 121 | updated, _ := time.Parse("2006-01-02 15:04", strings.Join(fields[len(fields)-3:len(fields)-2], " ")) 122 | var location string 123 | switch len(pieces) { 124 | case 3: 125 | location = fmt.Sprintf(SourceFormat3, pieces[0], version, name, version) 126 | case 4: 127 | location = fmt.Sprintf(SourceFormat4, pieces[0], pieces[1], version, name, version) 128 | case 5: 129 | location = fmt.Sprintf(SourceFormat5, pieces[0], pieces[1], version, pieces[3], name, version) 130 | case 6: 131 | location = fmt.Sprintf(SourceFormat6, pieces[0], pieces[1], version, pieces[3], pieces[4], name, version) 132 | } 133 | r := results.NewResult(name, version, location, updated) 134 | rs.AddResult(r) 135 | } 136 | break 137 | } 138 | if rs.Len() == 0 { 139 | err = results.NotFound 140 | return 141 | } 142 | err = nil 143 | return 144 | } 145 | -------------------------------------------------------------------------------- /providers/launchpad/files.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package launchpad 18 | 19 | // File is a JSON representation of a Launchpad File 20 | type File struct { 21 | Link string `json:"file_link"` 22 | Type string `json:"file_type"` 23 | Uploaded string `json:"date_uploaded"` 24 | } 25 | 26 | // FileList holds one or more Launchpad Files 27 | type FileList struct { 28 | Files []File `json:"entries"` 29 | } 30 | -------------------------------------------------------------------------------- /providers/launchpad/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package launchpad 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "github.com/DataDrake/cuppa/util" 23 | "regexp" 24 | ) 25 | 26 | const ( 27 | // FilesAPI is the format string for the Launchpad Files API 28 | FilesAPI = "https://api.launchpad.net/1.0/%s/%s/%s/files" 29 | // ReleasesAPI is the format string for the Launchpad Releases API 30 | ReleasesAPI = "https://api.launchpad.net/1.0/%s/%s/releases" 31 | // SeriesAPI is the format string for the Launchpad Series API 32 | SeriesAPI = "https://api.launchpad.net/1.0/%s/series" 33 | // SourceFormat is the format string for Launchpad tarballs 34 | SourceFormat = "https://launchpad.net/%s/%s/%s/+download/%s-%s.tar.gz" 35 | ) 36 | 37 | // SourceRegex matches Launchpad source tarballs 38 | var SourceRegex = regexp.MustCompile("https?://launchpad.net/(.*)/.*/.*/\\+download/.*.tar.gz") 39 | 40 | // Provider is the upstream provider interface for launchpad 41 | type Provider struct{} 42 | 43 | // String gives the name of this provider 44 | func (c Provider) String() string { 45 | return "Launchpad" 46 | } 47 | 48 | // Match checks to see if this provider can handle this kind of query 49 | func (c Provider) Match(query string) (params []string) { 50 | if sm := SourceRegex.FindStringSubmatch(query); len(sm) > 1 { 51 | params = sm[1:] 52 | } 53 | return 54 | } 55 | 56 | // Latest finds the newest release for a launchpad package 57 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 58 | rs, err := c.Releases(params) 59 | if err == nil { 60 | r = rs.Last() 61 | } 62 | return 63 | } 64 | 65 | // Releases finds all matching releases for a launchpad package 66 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 67 | name := params[0] 68 | // Query the API 69 | url := fmt.Sprintf(SeriesAPI, name) 70 | var seriesList SeriesList 71 | if err = util.FetchJSON(url, "series", &seriesList); err != nil { 72 | return 73 | } 74 | // Proccess Releases 75 | var lrs Releases 76 | for _, s := range seriesList.Entries { 77 | // Only Active Series 78 | if !s.Active { 79 | continue 80 | } 81 | // Only stable or supported 82 | switch s.Status { 83 | case "Active Development": 84 | case "Current Stable Release": 85 | case "Supported": 86 | default: 87 | continue 88 | } 89 | url := fmt.Sprintf(ReleasesAPI, name, s.Name) 90 | var vl VersionList 91 | if err = util.FetchJSON(url, "releases", &vl); err != nil { 92 | continue 93 | } 94 | for i := len(vl.Versions) - 1; i >= 0; i-- { 95 | r := vl.Versions[i] 96 | url := fmt.Sprintf(FilesAPI, name, s.Name, r.Number) 97 | var fl FileList 98 | if err = util.FetchJSON(url, "files", &fl); err != nil { 99 | continue 100 | } 101 | var lr Release 102 | for _, f := range fl.Files { 103 | if f.Type != "Code Release Tarball" { 104 | continue 105 | } 106 | lr.name = name 107 | lr.series = s.Name 108 | lr.release = r.Number 109 | lr.uploaded = f.Uploaded 110 | } 111 | lrs = append(lrs, lr) 112 | } 113 | } 114 | if len(lrs) == 0 { 115 | err = results.NotFound 116 | return 117 | } 118 | rs = lrs.Convert(name) 119 | err = nil 120 | return 121 | } 122 | -------------------------------------------------------------------------------- /providers/launchpad/releases.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package launchpad 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "time" 23 | ) 24 | 25 | // Release is an internal representation of a Launchpad Release 26 | type Release struct { 27 | name string 28 | release string 29 | series string 30 | uploaded string 31 | } 32 | 33 | // Convert turns a Launchpad release into a Cuppa result 34 | func (lr *Release) Convert() *results.Result { 35 | published, _ := time.Parse(time.RFC3339, lr.uploaded) 36 | location := fmt.Sprintf(SourceFormat, lr.name, lr.series, lr.release, lr.name, lr.release) 37 | return results.NewResult(lr.name, lr.release, location, published) 38 | } 39 | 40 | // Releases holds one or more Launchpad Releases 41 | type Releases []Release 42 | 43 | // Convert turns a Launchpad result set to a Cuppa result set 44 | func (lrs Releases) Convert(name string) *results.ResultSet { 45 | rs := results.NewResultSet(name) 46 | for _, rel := range lrs { 47 | if r := rel.Convert(); r != nil { 48 | rs.AddResult(r) 49 | } 50 | } 51 | return rs 52 | } 53 | -------------------------------------------------------------------------------- /providers/launchpad/series.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package launchpad 18 | 19 | // Series is a JSON representation of a Launchpad Series 20 | type Series struct { 21 | Active bool `json:"active"` 22 | Name string `json:"name"` 23 | Status string `json:"status"` 24 | } 25 | 26 | // SeriesList holds one or more Launchpad Series 27 | type SeriesList struct { 28 | Entries []Series `json:"entries"` 29 | } 30 | -------------------------------------------------------------------------------- /providers/launchpad/versions.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package launchpad 18 | 19 | // Version is a JSON representation of a Launchpad Version 20 | type Version struct { 21 | Number string `json:"version"` 22 | } 23 | 24 | // VersionList holds one or more Launchpad Version 25 | type VersionList struct { 26 | Versions []Version `json:"entries"` 27 | } 28 | -------------------------------------------------------------------------------- /providers/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package providers 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/providers/cpan" 21 | "github.com/DataDrake/cuppa/providers/git" 22 | "github.com/DataDrake/cuppa/providers/github" 23 | "github.com/DataDrake/cuppa/providers/gitlab" 24 | "github.com/DataDrake/cuppa/providers/gnome" 25 | "github.com/DataDrake/cuppa/providers/gnu" 26 | "github.com/DataDrake/cuppa/providers/hackage" 27 | "github.com/DataDrake/cuppa/providers/html" 28 | "github.com/DataDrake/cuppa/providers/jetbrains" 29 | "github.com/DataDrake/cuppa/providers/kde" 30 | "github.com/DataDrake/cuppa/providers/launchpad" 31 | "github.com/DataDrake/cuppa/providers/pypi" 32 | "github.com/DataDrake/cuppa/providers/rubygems" 33 | "github.com/DataDrake/cuppa/providers/sourceforge" 34 | "github.com/DataDrake/cuppa/results" 35 | ) 36 | 37 | // Provider provides a common interface for each of the backend providers 38 | type Provider interface { 39 | String() string 40 | Match(query string) []string 41 | Latest(params []string) (*results.Result, error) 42 | Releases(params []string) (*results.ResultSet, error) 43 | } 44 | 45 | // All returns a list of all available providers 46 | func All() []Provider { 47 | return []Provider{ 48 | cpan.Provider{}, 49 | github.Provider{}, 50 | gitlab.Provider{}, 51 | gnome.Provider{}, 52 | gnu.Provider{}, 53 | hackage.Provider{}, 54 | html.Provider{}, 55 | jetbrains.Provider{}, 56 | kde.Provider{}, 57 | launchpad.Provider{}, 58 | pypi.Provider{}, 59 | rubygems.Provider{}, 60 | sourceforge.Provider{}, 61 | git.Provider{}, // Git should be last to avoid using it unless necessary 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /providers/pypi/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package pypi 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "github.com/DataDrake/cuppa/util" 23 | "regexp" 24 | "strings" 25 | ) 26 | 27 | const ( 28 | // SourceAPI is the format string for the PyPi API 29 | SourceAPI = "https://pypi.python.org/pypi/%s/json" 30 | // DateFormat is the Time format used by PyPi 31 | DateFormat = "2006-01-02T15:04:05" 32 | ) 33 | 34 | // TarballRegex matches PyPi source tarballs 35 | var TarballRegex = regexp.MustCompile("https?://[^/]*py[^/]*/packages/(?:[^/]+/)+(.+)$") 36 | 37 | // Provider is the upstream provider interface for pypi 38 | type Provider struct{} 39 | 40 | // String gives the name of this provider 41 | func (c Provider) String() string { 42 | return "PyPi" 43 | } 44 | 45 | // Match checks to see if this provider can handle this kind of query 46 | func (c Provider) Match(query string) (params []string) { 47 | if sm := TarballRegex.FindStringSubmatch(query); len(sm) > 1 { 48 | pieces := strings.Split(sm[1], "-") 49 | if len(pieces) > 2 { 50 | params = append(params, strings.Join(pieces[0:len(pieces)-1], "-")) 51 | return 52 | } 53 | params = append(params, pieces[0]) 54 | } 55 | return 56 | } 57 | 58 | // Latest finds the newest release for a pypi package 59 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 60 | name := params[0] 61 | url := fmt.Sprintf(SourceAPI, name) 62 | var cr LatestSource 63 | if err = util.FetchJSON(url, "latest", &cr); err == nil { 64 | r = cr.Convert(name) 65 | } 66 | return 67 | } 68 | 69 | // Releases finds all matching releases for a pypi package 70 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 71 | name := params[0] 72 | url := fmt.Sprintf(SourceAPI, name) 73 | var crs Releases 74 | if err = util.FetchJSON(url, "releases", &crs); err != nil { 75 | return 76 | } 77 | if len(crs.Releases) == 0 { 78 | err = results.NotFound 79 | return 80 | } 81 | rs = crs.Convert(name) 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /providers/pypi/releases.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package pypi 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | "time" 22 | ) 23 | 24 | // ConvertURLS translates PyPi URLs to Cuppa results 25 | func ConvertURLS(cr []URL, name, version string) *results.Result { 26 | u := cr[len(cr)-1] 27 | published, _ := time.Parse(DateFormat, u.UploadTime) 28 | return results.NewResult(name, version, u.URL, published) 29 | } 30 | 31 | // Releases holds one or more Source URLs 32 | type Releases struct { 33 | Releases map[string][]URL `json:"releases"` 34 | } 35 | 36 | // Convert turns PyPi releases into a Cuppa results set 37 | func (crs *Releases) Convert(name string) *results.ResultSet { 38 | rs := results.NewResultSet(name) 39 | for ver, rel := range crs.Releases { 40 | if r := ConvertURLS(rel, name, ver); r != nil { 41 | rs.AddResult(r) 42 | } 43 | } 44 | return rs 45 | } 46 | -------------------------------------------------------------------------------- /providers/pypi/source.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package pypi 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | "time" 22 | ) 23 | 24 | // Info contains a PyPi Version number 25 | type Info struct { 26 | Version string `json:"version"` 27 | } 28 | 29 | // URL contains a JSON representation of a PyPi tarball URL 30 | type URL struct { 31 | UploadTime string `json:"upload_time"` 32 | URL string `json:"url"` 33 | } 34 | 35 | // LatestSource contains a JSON representation of a PyPi Source 36 | type LatestSource struct { 37 | Info Info `json:"info"` 38 | URLs []URL `json:"urls"` 39 | } 40 | 41 | // Convert turns a PyPi latest into a Cuppa Result 42 | func (cr *LatestSource) Convert(name string) *results.Result { 43 | u := cr.URLs[len(cr.URLs)-1] 44 | published, _ := time.Parse(DateFormat, u.UploadTime) 45 | return results.NewResult(name, cr.Info.Version, u.URL, published) 46 | } 47 | -------------------------------------------------------------------------------- /providers/rubygems/latest.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package rubygems 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "time" 23 | ) 24 | 25 | // LatestVersion is a JSON representation of the latest Version of a Gem 26 | type LatestVersion struct { 27 | Version string `json:"version"` 28 | } 29 | 30 | // Convert turns a Rubygems latest release into a Cuppa result 31 | func (cr *LatestVersion) Convert(name string) *results.Result { 32 | return results.NewResult(name, cr.Version, fmt.Sprintf(SourceFormat, name, cr.Version), time.Time{}) 33 | } 34 | -------------------------------------------------------------------------------- /providers/rubygems/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package rubygems 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "github.com/DataDrake/cuppa/util" 23 | "regexp" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | const ( 29 | // LatestAPI is the string format for the Rubygems latest API 30 | LatestAPI = "https://rubygems.org/api/v1/versions/%s/latest.json" 31 | // VersionsAPI is the string format for the Rubygems versions API 32 | VersionsAPI = "https://rubygems.org/api/v1/versions/%s.json" 33 | // SourceFormat is the string format for Gem sources 34 | SourceFormat = "https://rubygems.org/downloads/%s-%s.gem" 35 | ) 36 | 37 | // GemRegex matches Rubygems sources 38 | var GemRegex = regexp.MustCompile("https?://rubygems.org/downloads/(.+).gem") 39 | 40 | // Provider is the upstream provider interface for rubygems 41 | type Provider struct{} 42 | 43 | // String gives the name of this provider 44 | func (c Provider) String() string { 45 | return "Rubygems" 46 | } 47 | 48 | // Match checks to see if this provider can handle this kind of query 49 | func (c Provider) Match(query string) (params []string) { 50 | if sm := GemRegex.FindStringSubmatch(query); len(sm) > 1 { 51 | pieces := strings.Split(sm[1], "-") 52 | if len(pieces) > 2 { 53 | params = append(params, strings.Join(pieces[0:len(pieces)-1], "-")) 54 | return 55 | } 56 | params = pieces[:1] 57 | } 58 | return 59 | } 60 | 61 | // Latest finds the newest release for a rubygems package 62 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 63 | name := params[0] 64 | url := fmt.Sprintf(LatestAPI, name) 65 | var cr LatestVersion 66 | if err = util.FetchJSON(url, "latest", &cr); err == nil { 67 | r = cr.Convert(name) 68 | } 69 | time.Sleep(time.Second) 70 | return 71 | } 72 | 73 | // Releases finds all matching releases for a rubygems package 74 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 75 | name := params[0] 76 | url := fmt.Sprintf(VersionsAPI, name) 77 | var crs Versions 78 | if err = util.FetchJSON(url, "releases", &crs); err != nil { 79 | return 80 | } 81 | if len(crs) == 0 { 82 | err = results.NotFound 83 | return 84 | } 85 | rs = crs.Convert(name) 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /providers/rubygems/version.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package rubygems 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/results" 22 | "time" 23 | ) 24 | 25 | // Version is a JSON representation of a version of a Gem 26 | type Version struct { 27 | CreatedAt string `json:"created_at"` 28 | PreRelease bool `json:"prerelease"` 29 | Number string `json:"number"` 30 | } 31 | 32 | // Convert turns a Rubygems version to a Cuppa result 33 | func (cr *Version) Convert(name string) *results.Result { 34 | if cr.PreRelease { 35 | return nil 36 | } 37 | published, _ := time.Parse(time.RFC3339, cr.CreatedAt) 38 | location := fmt.Sprintf(SourceFormat, name, cr.Number) 39 | return results.NewResult(name, cr.Number, location, published) 40 | } 41 | -------------------------------------------------------------------------------- /providers/rubygems/versions.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package rubygems 18 | 19 | import ( 20 | "github.com/DataDrake/cuppa/results" 21 | ) 22 | 23 | // Versions holds one or more Rubygems Versions 24 | type Versions []Version 25 | 26 | // Convert turns a Rubygems result set into a Cuppa result set 27 | func (crs *Versions) Convert(name string) *results.ResultSet { 28 | rs := results.NewResultSet(name) 29 | for _, rel := range *crs { 30 | if r := rel.Convert(name); r != nil { 31 | rs.AddResult(r) 32 | } 33 | } 34 | return rs 35 | } 36 | -------------------------------------------------------------------------------- /providers/sourceforge/provider.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package sourceforge 18 | 19 | import ( 20 | "encoding/xml" 21 | "fmt" 22 | "github.com/DataDrake/cuppa/results" 23 | log "github.com/DataDrake/waterlog" 24 | "net/http" 25 | "regexp" 26 | "time" 27 | ) 28 | 29 | const ( 30 | // API is the format string for a sourceforge RSS feed 31 | API = "https://sourceforge.net/projects/%s/rss?path=/%s" 32 | ) 33 | 34 | var ( 35 | // TarballRegex matches SourceForge sources 36 | TarballRegex = regexp.MustCompile("https?://.*sourceforge.net/projects?/(.+)/files/(.+/)?(.+?)[\\-_]([\\d]+(?:.\\d+)*\\w*?)\\.(?:zip|tar\\..+z.*)(?:\\/download)?$") 37 | // ProjectRegex matches SourceForge sources 38 | ProjectRegex = regexp.MustCompile("https?://.*sourceforge.net/projects?/(.+)/(?:files/)?(.+?/)?(.+?)[\\-_]([\\d]+(?:.\\d+)*\\w*?).+$") 39 | ) 40 | 41 | // Item represents an entry in the RSS Feed 42 | type Item struct { 43 | XMLName xml.Name `xml:"item"` 44 | Link string `xml:"link"` 45 | Date string `xml:"pubDate"` 46 | } 47 | 48 | // Feed represents the RSS feed itself 49 | type Feed struct { 50 | XMLName xml.Name `xml:"rss"` 51 | Items []Item `xml:"channel>item"` 52 | } 53 | 54 | // toResults converts a Feed to a ResultSet 55 | func (f *Feed) toResults(name string) *results.ResultSet { 56 | rs := results.NewResultSet(name) 57 | for _, item := range f.Items { 58 | if sm := TarballRegex.FindStringSubmatch(item.Link); len(sm) > 4 { 59 | pub, _ := time.Parse(time.RFC1123, item.Date+"C") 60 | r := results.NewResult(name, sm[4], item.Link, pub) 61 | rs.AddResult(r) 62 | } 63 | } 64 | return rs 65 | } 66 | 67 | // Provider is the upstream provider interface for SourceForge 68 | type Provider struct{} 69 | 70 | // String gives the name of this provider 71 | func (c Provider) String() string { 72 | return "SourceForge" 73 | } 74 | 75 | // Match checks to see if this provider can handle this kind of query 76 | func (c Provider) Match(query string) (params []string) { 77 | sm := TarballRegex.FindStringSubmatch(query) 78 | if len(sm) != 5 { 79 | sm = ProjectRegex.FindStringSubmatch(query) 80 | } 81 | if len(sm) == 5 { 82 | params = append(params, query) 83 | } 84 | return 85 | } 86 | 87 | // Latest finds the newest release for a SourceForge package 88 | func (c Provider) Latest(params []string) (r *results.Result, err error) { 89 | rs, err := c.Releases(params) 90 | if err == nil { 91 | r = rs.First() 92 | } 93 | return 94 | } 95 | 96 | // Releases finds all matching releases for a SourceForge package 97 | func (c Provider) Releases(params []string) (rs *results.ResultSet, err error) { 98 | name := params[0] 99 | sm := TarballRegex.FindStringSubmatch(name) 100 | if len(sm) != 5 { 101 | sm = ProjectRegex.FindStringSubmatch(name) 102 | sm[1], sm[3] = sm[3], sm[1] 103 | } 104 | // Query the API 105 | resp, err := http.Get(fmt.Sprintf(API, sm[1], "")) 106 | if err != nil { 107 | log.Debugf("Failed to get releases: %s\n", err) 108 | err = results.Unavailable 109 | return 110 | } 111 | defer resp.Body.Close() 112 | // Translate Status Code 113 | switch resp.StatusCode { 114 | case 200: 115 | break 116 | case 404: 117 | err = results.NotFound 118 | return 119 | default: 120 | err = results.Unavailable 121 | return 122 | } 123 | // decode response 124 | dec := xml.NewDecoder(resp.Body) 125 | var feed Feed 126 | if err = dec.Decode(&feed); err != nil { 127 | log.Debugf("Failed to decode releases: %s\n", err) 128 | err = results.Unavailable 129 | return 130 | } 131 | rs = feed.toResults(sm[3]) 132 | if rs.Len() == 0 { 133 | err = results.NotFound 134 | } 135 | return 136 | } 137 | -------------------------------------------------------------------------------- /results/result.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package results 18 | 19 | import ( 20 | "fmt" 21 | "github.com/DataDrake/cuppa/version" 22 | "os" 23 | "text/tabwriter" 24 | "time" 25 | ) 26 | 27 | // Result contains the information for a single query result 28 | type Result struct { 29 | Name string 30 | Version version.Version 31 | Location string 32 | Published time.Time 33 | } 34 | 35 | // NewResult creates a result with the specified values 36 | func NewResult(name, v string, location string, published time.Time) *Result { 37 | r := &Result{name, version.NewVersion(v), location, published} 38 | if r.Published.IsZero() { 39 | r.Published = r.Version.FindDate() 40 | } 41 | return r 42 | } 43 | 44 | // Print pretty-prints a single Result 45 | func (r *Result) Print() { 46 | tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) 47 | fmt.Fprintf(tw, "%s\t: %s\n", "Name", r.Name) 48 | fmt.Fprintf(tw, "%s\t: %s\n", "Version", r.Version) 49 | if r.Location != "" { 50 | fmt.Fprintf(tw, "%s\t: %s\n", "Location", r.Location) 51 | } 52 | if !r.Published.IsZero() { 53 | fmt.Fprintf(tw, "%s\t: %s\n", "Published", r.Published.Format(time.RFC3339)) 54 | } 55 | tw.Flush() 56 | fmt.Println() 57 | } 58 | 59 | // PrintSimple only prints the version and the location of the latest release 60 | func (r *Result) PrintSimple() { 61 | fmt.Printf("%s %s\n", r.Version, r.Location) 62 | } 63 | -------------------------------------------------------------------------------- /results/resultset.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package results 18 | 19 | import ( 20 | "fmt" 21 | "sort" 22 | ) 23 | 24 | // ResultSet is a collection of the Results of a Provider query 25 | type ResultSet struct { 26 | results []*Result 27 | query string 28 | } 29 | 30 | // NewResultSet creates as empty ResultSet for the provided query 31 | func NewResultSet(query string) *ResultSet { 32 | return &ResultSet{make([]*Result, 0), query} 33 | } 34 | 35 | var skipWords = []string{ 36 | "master", "Master", "MASTER", 37 | "rc", "RC", 38 | "alpha", "Alpha", "ALPHA", 39 | "beta", "Beta", "BETA", 40 | "dev", "DEV", 41 | "unstable", "Unstable", "UNSTABLE", 42 | "eap", "EAP", 43 | "donotuse", 44 | } 45 | 46 | // AddResult appends a new Result 47 | func (rs *ResultSet) AddResult(r *Result) { 48 | if r == nil || r.Version[0] == "N/A" { 49 | return 50 | } 51 | for _, part := range r.Version { 52 | for _, skip := range skipWords { 53 | if part == skip { 54 | return 55 | } 56 | } 57 | } 58 | rs.results = append(rs.results, r) 59 | } 60 | 61 | // First retrieves the first result from a query 62 | func (rs *ResultSet) First() *Result { 63 | sort.Sort(rs) 64 | return rs.results[0] 65 | } 66 | 67 | // Last retrieves the first result from a query 68 | func (rs *ResultSet) Last() *Result { 69 | if rs.Len() == 0 { 70 | return nil 71 | } 72 | sort.Sort(rs) 73 | return rs.results[len(rs.results)-1] 74 | } 75 | 76 | // PrintAll pretty-prints an entire ResultSet 77 | func (rs *ResultSet) PrintAll() { 78 | fmt.Printf("%s: '%s'\n", "Results of Query", rs.query) 79 | fmt.Printf("%s: %d\n\n", "Total Number of Results", rs.Len()) 80 | sort.Sort(rs) 81 | for _, r := range rs.results { 82 | r.Print() 83 | } 84 | } 85 | 86 | // Len is the number of elements in the ResultSet (sort.Interface) 87 | func (rs *ResultSet) Len() int { 88 | return len(rs.results) 89 | } 90 | 91 | // Less reports whether the element with 92 | // index i should sort before the element with index j. (sort.Interface) 93 | func (rs *ResultSet) Less(i, j int) bool { 94 | if !rs.results[i].Published.IsZero() && !rs.results[j].Published.IsZero() { 95 | return rs.results[i].Published.Before(rs.results[j].Published) 96 | } 97 | return !rs.results[i].Version.Less(rs.results[j].Version) 98 | } 99 | 100 | // Swap swaps the elements with indexes i and j. 101 | func (rs *ResultSet) Swap(i, j int) { 102 | rs.results[i], rs.results[j] = rs.results[j], rs.results[i] 103 | } 104 | -------------------------------------------------------------------------------- /results/status.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package results 18 | 19 | import ( 20 | "errors" 21 | ) 22 | 23 | var ( 24 | // NotFound - Query completed successfully, without results 25 | NotFound = errors.New("not found") 26 | // Unavailable - Provider could not be reached 27 | Unavailable = errors.New("could not reach provider") 28 | ) 29 | -------------------------------------------------------------------------------- /util/http.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package util 18 | 19 | import ( 20 | "encoding/json" 21 | "github.com/DataDrake/cuppa/results" 22 | log "github.com/DataDrake/waterlog" 23 | "net/http" 24 | ) 25 | 26 | // FetchJSON requests from a URL and converts the message body from JSON to a desired type 27 | func FetchJSON(url, kind string, out interface{}) error { 28 | req, err := http.NewRequest("GET", url, nil) 29 | if err != nil { 30 | log.Debugf("Failed to build request: %s\n", err) 31 | return results.Unavailable 32 | } 33 | req.Header.Set("Accept", "application/json") 34 | resp, err := http.DefaultClient.Do(req) 35 | if err != nil { 36 | log.Debugf("Failed to get %s: %s\n", kind, err) 37 | return results.Unavailable 38 | } 39 | defer resp.Body.Close() 40 | // Translate Status Code 41 | switch resp.StatusCode { 42 | case 200: 43 | break 44 | case 404: 45 | return results.NotFound 46 | default: 47 | return results.Unavailable 48 | } 49 | // Decode response 50 | dec := json.NewDecoder(resp.Body) 51 | if err = dec.Decode(out); err != nil { 52 | log.Debugf("Failed to decode response: %s\n", err) 53 | return results.Unavailable 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package version 18 | 19 | import ( 20 | "strconv" 21 | "strings" 22 | "time" 23 | "unicode" 24 | ) 25 | 26 | // Version is a record of a new version for a single source 27 | type Version []string 28 | 29 | func splitDigit(raw string) []string { 30 | pieces := make([]string, 0) 31 | if len(raw) == 0 { 32 | return pieces 33 | } 34 | i := 0 35 | for _, char := range raw { 36 | if !unicode.IsDigit(char) { 37 | break 38 | } 39 | i++ 40 | } 41 | if i > 0 { 42 | pieces = append(pieces, raw[:i]) 43 | } 44 | if i < len(raw) { 45 | pieces = append(pieces, splitChar(raw[i:])...) 46 | } 47 | return pieces 48 | } 49 | 50 | func splitChar(raw string) []string { 51 | pieces := make([]string, 0) 52 | if len(raw) == 0 { 53 | return pieces 54 | } 55 | i := 0 56 | for _, char := range raw { 57 | if unicode.IsDigit(char) { 58 | break 59 | } 60 | i++ 61 | } 62 | if i > 0 { 63 | pieces = append(pieces, raw[:i]) 64 | } 65 | if i < len(raw) { 66 | pieces = append(pieces, splitDigit(raw[i:])...) 67 | } 68 | return pieces 69 | } 70 | 71 | // NewVersion creates a new version by parsing from a string 72 | func NewVersion(raw string) Version { 73 | dots := strings.Split(raw, ".") 74 | dashes := make([]string, 0) 75 | for _, dot := range dots { 76 | dashes = append(dashes, strings.Split(dot, "-")...) 77 | } 78 | unclean := make([]string, 0) 79 | for _, dash := range dashes { 80 | unclean = append(unclean, strings.Split(dash, "_")...) 81 | } 82 | pieces := make([]string, 0) 83 | for _, u := range unclean { 84 | if u != "" { 85 | pieces = append(pieces, u) 86 | } 87 | } 88 | if len(pieces) == 0 { 89 | return []string{"N/A"} 90 | } 91 | v := make(Version, 0) 92 | started := false 93 | for _, piece := range pieces { 94 | parts := splitChar(piece) 95 | for _, part := range parts { 96 | if !started && !unicode.IsDigit(rune(part[0])) { 97 | continue 98 | } 99 | started = true 100 | v = append(v, part) 101 | } 102 | } 103 | if len(v) == 0 { 104 | v = append(v, "N/A") 105 | } else { 106 | // Strip trailing words 107 | i := len(v) - 1 108 | for i >= 0 && !unicode.IsDigit(rune(v[i][0])) { 109 | i-- 110 | } 111 | v = v[:i+1] 112 | } 113 | return v 114 | } 115 | 116 | // Compare allows to version nubmers to be compared to see which is newer (higher) 117 | func (v Version) Compare(old Version) int { 118 | result := 0 119 | for i, piece := range v { 120 | if len(old) == i { 121 | return -1 122 | } 123 | if old[i] == piece { 124 | continue 125 | } 126 | curr, e1 := strconv.Atoi(piece) 127 | prev, e2 := strconv.Atoi(old[i]) 128 | if e1 != nil && e2 != nil { 129 | goto HARD 130 | } 131 | if e1 != nil { 132 | return -1 133 | } 134 | if e2 != nil { 135 | return 1 136 | } 137 | result = prev - curr 138 | goto CHECK 139 | HARD: 140 | result = strings.Compare(piece, old[i]) 141 | CHECK: 142 | if result != 0 { 143 | return result 144 | } 145 | } 146 | return result 147 | } 148 | 149 | // Less checks if this version is less than another 150 | func (v Version) Less(other Version) bool { 151 | return v.Compare(other) < 0 152 | } 153 | 154 | const dateLayout = "20060102" 155 | 156 | // FindDate tries to parse a version into a date if possible 157 | func (v Version) FindDate() time.Time { 158 | t, err := time.Parse(dateLayout, v[0]) 159 | if err == nil { 160 | return t 161 | } 162 | thisYear, _, _ := time.Now().Date() 163 | year, err := strconv.Atoi(v[0]) 164 | if err != nil { 165 | return time.Time{} 166 | } 167 | if year > thisYear || year < (thisYear-20) { 168 | return time.Time{} 169 | } 170 | var month, day int 171 | if len(v) > 1 { 172 | month, _ = strconv.Atoi(v[1]) 173 | } 174 | if len(v) > 2 { 175 | day, _ = strconv.Atoi(v[2]) 176 | } 177 | return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) 178 | } 179 | 180 | // String converts a version to a string for printing 181 | func (v Version) String() string { 182 | return strings.Join(v, ".") 183 | } 184 | -------------------------------------------------------------------------------- /version/version_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2016-2021 Bryan T. Meyers 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package version 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func newVersionTest(t *testing.T, raw string, actual Version) { 24 | v := NewVersion(raw) 25 | if len(v) != len(actual) { 26 | t.Logf("%#v", v) 27 | t.Errorf("Expected length '%d', found '%d'", len(actual), len(v)) 28 | } 29 | for i, piece := range actual { 30 | if v[i] != piece { 31 | t.Error("Versions do not match") 32 | } 33 | } 34 | } 35 | 36 | const rawVersion1 = "1.2.3.4" 37 | 38 | var version1 = Version{"1", "2", "3", "4"} 39 | 40 | func TestNewVersion1(t *testing.T) { 41 | newVersionTest(t, rawVersion1, version1) 42 | } 43 | 44 | const rawVersion2 = "v1.2.3.4." 45 | 46 | var version2 = Version{"1", "2", "3", "4"} 47 | 48 | func TestNewVersion2(t *testing.T) { 49 | newVersionTest(t, rawVersion2, version2) 50 | } 51 | 52 | const rawVersion3 = "release-1.2.3.5" 53 | 54 | var version3 = Version{"1", "2", "3", "5"} 55 | 56 | func TestNewVersion3(t *testing.T) { 57 | newVersionTest(t, rawVersion3, version3) 58 | } 59 | 60 | const rawVersion4 = "v1.2-bob-4" 61 | 62 | var version4 = Version{"1", "2", "bob", "4"} 63 | 64 | func TestNewVersion4(t *testing.T) { 65 | newVersionTest(t, rawVersion4, version4) 66 | } 67 | 68 | const rawVersion5 = "v1.2.3rc1" 69 | 70 | var version5 = Version{"1", "2", "3", "rc", "1"} 71 | 72 | func TestNewVersion5(t *testing.T) { 73 | newVersionTest(t, rawVersion5, version5) 74 | } 75 | 76 | func TestVersionCompareEqual1(t *testing.T) { 77 | if c := version1.Compare(version1); c != 0 { 78 | t.Errorf("Should be equal, found '%d'", c) 79 | } 80 | } 81 | 82 | func TestVersionCompareEqual2(t *testing.T) { 83 | if c := version5.Compare(version5); c != 0 { 84 | t.Errorf("Should be equal, found '%d'", c) 85 | } 86 | } 87 | 88 | func TestVersionCompare1(t *testing.T) { 89 | if version1.Compare(version3) <= 0 { 90 | t.Error("Should have been less") 91 | } 92 | if version3.Compare(version1) >= 0 { 93 | t.Error("Should have been greater") 94 | } 95 | } 96 | 97 | func TestVersionCompare2(t *testing.T) { 98 | if version5.Compare(version1) >= 0 { 99 | t.Error("Should have been less") 100 | } 101 | if version1.Compare(version5) <= 0 { 102 | t.Error("Should have been greater") 103 | } 104 | } 105 | 106 | func TestVersionLess1(t *testing.T) { 107 | if version5.Less(version5) { 108 | t.Error("Should not be less: equal") 109 | } 110 | } 111 | 112 | func TestVersionLess2(t *testing.T) { 113 | if version1.Less(version5) { 114 | t.Error("Should not be less: greater") 115 | } 116 | } 117 | 118 | func TestVersionLess3(t *testing.T) { 119 | if !version5.Less(version1) { 120 | t.Error("Should be less: less") 121 | } 122 | } 123 | --------------------------------------------------------------------------------