├── .github └── workflows │ └── python.yml ├── .gitignore ├── README.md ├── main.py ├── requirements.txt ├── test-requirements.txt ├── test_main.py └── vercel.json /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Python 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: '3.9' 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r requirements.txt 24 | pip install -r test-requirements.txt 25 | - name: Test with pytest 26 | working-directory: . 27 | run: | 28 | pytest 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glare 2 | 3 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2FContextualist%2Fglare&project-name=glare&repo-name=glare&demo-title=Glare&demo-description=gracefully%20download%20(latest)%20releases%20from%20GitHub&demo-url=https%3A%2F%2Fgithub.com%2FContextualist%2Fglare&demo-image=https%3A%2F%2Frepository-images.githubusercontent.com%2F77979589%2Fa6437980-6472-11e9-9660-133b39d9978f) 4 | 5 | A little service for you to download releases from GitHub gracefully. Simply make a get request to Glare with the repo name (with an optional version) and release file name regex, and she will lead you to the way. 6 | 7 | **NOTE:** You might want to use [GitHub's direct link](https://help.github.com/en/articles/linking-to-releases#linking-to-the-latest-release) to the latest release asset (e.g. `github.com/{owner}/{repo}/releases/latest/download/asset-name.zip`) if the asset name is a constant string. Otherwise Glare is still helpful for matching the asset with regex. 8 | 9 | ### Demo 10 | The following will redirect you to `https://github.com/xtaci/kcptun/releases/download/{latest_version_tag}/kcptun-linux-amd64-{latest_version_number}.tar.gz`, and download the latest release of kcptun for linux-amd64. 11 | ```bash 12 | curl -fLO https://glare.now.sh/xtaci/kcptun/linux-amd64 13 | ``` 14 | 15 | Or you might want to have a version constraint: 16 | ```bash 17 | curl -fLO https://glare.now.sh/v2fly/v2ray-core@v4.x/linux-64 18 | ``` 19 | 20 | ## Motivation 21 | Sometimes when I'm writing a Dockerfile, I need to install packages from their GitHub latest releases. A neat way is to parse JSON responses from GitHub API with [`jq`](https://stedolan.github.io/jq) and get the desire link. Such way requires downlaoding `jq` from GitHub (The binary from Alpine apk is lack of regex feature). Still, the expression with `jq` is not clear enough, and parsing JSON with `sed` is way dirtier. So I spend a little time to write Glare. I hope she will save you a few minutes or from a frustring moment. 22 | 23 | ## Usage 24 | ``` 25 | # To get the latest release... 26 | /{owner}/{repo}/{file_name_regex} 27 | 28 | # To get a specific version or pick within a version range... 29 | /{owner}/{repo}@{tag|semver}/{file_name_regex} 30 | ``` 31 | `{file_name_regex}` is a regular expression to match the file (or specially, it can be `tar` or `zip` standing for the source code download in the respective format). It should match at least one file among the latest release files, otherwise Glare will throw an error. If multiple files are matched, Glare returns the one with shortest length. 32 | 33 | If `{tag}` is given, Glare looks for a release with exact matching tag. For `{semver}` provided, Glare treats it as a [npm-flavor semver](https://semver.npmjs.com/) and matches all release tag names against it. The highest of all satisfied versions is chosen. 34 | 35 | Tip: to check if a request leads to the desired redirection, `curl` it without any option. 36 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, redirect 2 | import httpx 3 | import semver 4 | import re 5 | 6 | app = Flask(__name__) 7 | 8 | @app.route('///') 9 | def get_release(user, repo_ver, name_re): 10 | try: 11 | name_reobj = re.compile(name_re) 12 | except re.error as e: 13 | return jsonify(message=f"bad regular expression: {e.msg}"), 400 14 | 15 | repo_ver = repo_ver.split('@', 1) 16 | repo = repo_ver[0] 17 | if len(repo_ver) == 2: # versioned 18 | ver = repo_ver[1] 19 | all_releases, err = api_req(f"https://api.github.com/repos/{user}/{repo}/releases?per_page=100") 20 | if err: return err 21 | all_tags = [x['tag_name'] for x in all_releases] 22 | if ver in all_tags: # exact match 23 | tag = f"tags/{ver}" 24 | else: 25 | try: 26 | v = semver.max_satisfying(all_tags, ver) 27 | except Exception as e: 28 | return jsonify(message=f"error matching the tag: {e}"), 400 29 | if v is None: 30 | return jsonify(message="no tag matched"), 404 31 | tag = f"tags/{v}" 32 | else: 33 | tag = "latest" 34 | 35 | release, err = api_req(f"https://api.github.com/repos/{user}/{repo}/releases/{tag}") 36 | if err: return err 37 | 38 | if name_re == 'tar': 39 | return redirect(release['tarball_url']) 40 | if name_re == 'zip': 41 | return redirect(release['zipball_url']) 42 | assets = release['assets'] 43 | matched = [x['browser_download_url'] for x in assets if name_reobj.search(x['name'])] 44 | if len(matched) == 0: 45 | return jsonify(message="no file matched"), 404 46 | matched.sort(key=len) 47 | return redirect(matched[0]) 48 | 49 | def api_req(url): 50 | resp = httpx.get(url) 51 | if resp.status_code != 200: 52 | return None, (jsonify(message="error from GitHub API", 53 | github_api_msg=resp.json()), resp.status_code) 54 | return resp.json(), None 55 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | httpx 3 | node-semver~=0.8.0 4 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | result 3 | -------------------------------------------------------------------------------- /test_main.py: -------------------------------------------------------------------------------- 1 | from main import app 2 | from result import Ok, Err, Result 3 | 4 | def app_context(tfn): 5 | def into_result(rv) -> Result[str, str]: 6 | if (redirect := rv.headers.get('Location')) is not None: 7 | return Ok(redirect) 8 | return Err(rv.get_json()['message']) 9 | def __wrapped(): 10 | with app.test_client() as c: 11 | tfn(lambda url: into_result(c.get(url))) 12 | return __wrapped 13 | 14 | ok = lambda r: r.expect(f"Expect Ok, got {r}") 15 | err = lambda r: r.expect_err(f"Expect Err, got {r}") 16 | 17 | @app_context 18 | def test_regex(get): 19 | assert ok(get(r"/v2fly/v2ray-core/macos-64\.zip$")).endswith("v2ray-macos-64.zip") 20 | assert err(get(r"/_user/_repo/[0-9.*\.tar\.gz")).startswith("bad regular expression") 21 | 22 | @app_context 23 | def test_github(get): 24 | assert err(get(r"/_user/_repo/_file")) == "error from GitHub API" 25 | 26 | @app_context 27 | def test_file_match(get): 28 | assert "kcptun-darwin-amd64" in ok(get(r"/xtaci/kcptun/darwin-amd64")) 29 | assert "zipball" in ok(get(r"/xtaci/kcptun/zip")) # zip/tar 30 | assert err(get(r"/xtaci/kcptun/plan9")) == "no file matched" 31 | assert ok(get(r"/v2fly/v2ray-core/macos-64")).endswith("v2ray-macos-64.zip") # shortest of multiple 32 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "builds": [ 3 | { "src": "main.py", "use": "@vercel/python" } 4 | ], 5 | "routes": [ 6 | { "src": "/.*/.*/.*", "dest": "main.py" } 7 | ] 8 | } 9 | --------------------------------------------------------------------------------