Compare commits
421 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c56b4d580 | |||
| 32439ca2f0 | |||
| fa49bcaf8b | |||
| 89e8e94bf1 | |||
| 5b8eb4735c | |||
| 1c9f000576 | |||
| de53d8762f | |||
| daa56efe27 | |||
| f1db010a68 | |||
| dbb5906767 | |||
| 18e414ec45 | |||
| fc00a1b3a8 | |||
| 556b74f4fc | |||
| 6e2e2d1665 | |||
| 9242350f0e | |||
| b0c14aa8f9 | |||
| 4b2516532e | |||
| 1aec8d727c | |||
| e60e0ba3c4 | |||
| 5877048749 | |||
| c8c1f05328 | |||
| 96f3e888cf | |||
| 14888f1cb8 | |||
| 49171c5e8c | |||
| 51fb9495ad | |||
| 89c1b3b4d8 | |||
| 1d80c12e18 | |||
| d092fc8436 | |||
| 8a812cd8ec | |||
| ae13b4e420 | |||
| 548c4d4efe | |||
| f6ed2feab4 | |||
| b730aa520d | |||
| 3d16c3e4e6 | |||
| 0d935a863f | |||
| 053b1d0f8d | |||
| b247f3f089 | |||
| 20ec435d6d | |||
| 53cd369b05 | |||
| ec3c604a83 | |||
| c3f871b49b | |||
| e5b2eab727 | |||
| bbf93430b7 | |||
| 3cb04b2c56 | |||
| e3c28a6c96 | |||
| c2571f9f47 | |||
| 4855fbd13f | |||
| 9569666259 | |||
| 42edd357bc | |||
| b33706006a | |||
| 0de80c2ff0 | |||
| 5d65a85bdb | |||
| a352aff93b | |||
| 713d575bad | |||
| 421bb467d0 | |||
| dde2ca06c3 | |||
| 091308787b | |||
| 04f9c456d5 | |||
| 6b7ffe0e26 | |||
| f29940d784 | |||
| 0413daf76b | |||
| 5043749b9f | |||
| 793f475edd | |||
| 03cfbfe4ac | |||
| e8d0360e64 | |||
| 05fd83bd6f | |||
| 864dd9f219 | |||
| 9a9a271b1f | |||
| 89d17107af | |||
| b6ddaae82a | |||
| 0bece2df7d | |||
| 4464802947 | |||
| 7c44238bae | |||
| b45c1cf327 | |||
| f1d7385294 | |||
| 77f5b911b5 | |||
| f8ca825524 | |||
| 6377f3a8b3 | |||
| 533d5dfc98 | |||
| 1ed3816d6f | |||
| 4ff52bd730 | |||
| 02a4c9db53 | |||
| 433de17562 | |||
| 23e4183bec | |||
| 926ddf6d09 | |||
| a6017a8411 | |||
| 608443b77f | |||
| 11e110bc7d | |||
| 806b9545a9 | |||
| b35fece3d5 | |||
| 4468ef1410 | |||
| e45cc2d2aa | |||
| 291bf96d15 | |||
| f0610bdd9c | |||
| ab82c4b300 | |||
| 94eaf0e254 | |||
| 9a6c80f562 | |||
| 9a4b045bdc | |||
| d3cfa6396d | |||
| 60a7362327 | |||
| 7be9f5989e | |||
| 651ee02c11 | |||
| fd9eaa3e83 | |||
| 83f3ccab0e | |||
| ae74f94655 | |||
| 989ca163a3 | |||
| 5b74083b5f | |||
| f88e93a0b9 | |||
| 2cf9bde1af | |||
| e589a35c67 | |||
| 4e5240c5ac | |||
| 5922a7a051 | |||
| 70effdc706 | |||
| a72aca1d05 | |||
| 351d157533 | |||
| 9493fb111f | |||
| a026efce6e | |||
| 1908c84fbe | |||
| f722fd1208 | |||
| 49feb1958c | |||
| e17d395ae7 | |||
| eeeea74bf1 | |||
| 52a626d7b9 | |||
| b8b6c74d86 | |||
| de7a613098 | |||
| b1c9977711 | |||
| a108cb7808 | |||
| 4f4f573955 | |||
| eb77fcfb72 | |||
| 5e59fcbd4f | |||
| 246a48359c | |||
| 40e983664c | |||
| 1eb239bf88 | |||
| d5afdf7691 | |||
| b539eb8575 | |||
| b8f7f44bb0 | |||
| 2249de439e | |||
| 4f358aa0f3 | |||
| 2f3708d40c | |||
| 46c13b5185 | |||
| d70b4d39ab | |||
| a37a2dd043 | |||
| 519fbcf581 | |||
| e74bf260c6 | |||
| 99f20868ad | |||
| b30a045a4e | |||
| b50778d3eb | |||
| 5ed1d7537a | |||
| a2021b74ba | |||
| 80b68fb126 | |||
| f87b05c90f | |||
| 73d73dc61e | |||
| 578cd8b24b | |||
| bae68520a2 | |||
| 6e20d37dda | |||
| cc8eb89ce0 | |||
| 84b83fc3c8 | |||
| 3afdf25012 | |||
| 5c3e78f605 | |||
| 6117a2f3cd | |||
| 997006cdd7 | |||
| 37bc451741 | |||
| 93f90dae68 | |||
| 7da72cf89a | |||
| ded70cd250 | |||
| 31435ef83e | |||
| d75ff0c5ab | |||
| c0eb66fe22 | |||
| c394a61a4e | |||
| 16b7b3bc3e | |||
| 065331c77f | |||
| 2b74c6779a | |||
| 0d66f2c6d3 | |||
| 6d25fb715b | |||
| 0ddfa5f7d9 | |||
| b015887b02 | |||
| 75b11d5744 | |||
| eb026ae4ac | |||
| 38d3b28445 | |||
| 0d0395b4e3 | |||
| e6c6ee441a | |||
| bfb96d536e | |||
| ad66250734 | |||
| acd318b640 | |||
| 55fad7ea27 | |||
| debbf5a5b0 | |||
| dd3078dc69 | |||
| d9f52bb634 | |||
| 04c4890124 | |||
| 1c103337fe | |||
| 13c89c29c5 | |||
| df7a6e18fd | |||
| a8de067810 | |||
| cf7712acea | |||
| 0f258fd55b | |||
| bf2f7d1e16 | |||
| e1b1618015 | |||
| e5d09491d4 | |||
| e1c9e6ccb4 | |||
| 5947e2a664 | |||
| e95c6ce8f4 | |||
| c1e3783272 | |||
| eaf1fc896f | |||
| e97ebfd8c2 | |||
| cc281f3533 | |||
| 4e07b549a2 | |||
| 019cf79704 | |||
| a102843789 | |||
| bf443a65e2 | |||
| 5ac55518ab | |||
| 514f518d2a | |||
| fc75b0cd8e | |||
| 7380c64141 | |||
| 943e7a7317 | |||
| 8fb71366f8 | |||
| 74c95ed34b | |||
| 82b3ea31e6 | |||
| 1b5f217e9c | |||
| ee5a8a05c9 | |||
| 499fb1f7df | |||
| 5f266319a3 | |||
| e32fa059e5 | |||
| 3371b15631 | |||
| c0e407e156 | |||
| d2f4ed93df | |||
| 1d6b0491a7 | |||
| c70bebb2aa | |||
| f7273e5cdf | |||
| 8304666261 | |||
| ec18abc97f | |||
| adbebd4de8 | |||
| e44b4b2540 | |||
| 248cb38390 | |||
| 506c2ab1c9 | |||
| 43bf6696d5 | |||
| f52f00abcf | |||
| da4ea18003 | |||
| 5e5615a2d1 | |||
| fe8347f32a | |||
| d2b360f7b3 | |||
| c9618458cb | |||
| ca918bc7e3 | |||
| c820197667 | |||
| 6c6ca69141 | |||
| 57f62b4797 | |||
| 1811522473 | |||
| 8e500e8bef | |||
| a2abd38ccf | |||
| 7fb0bdc9a5 | |||
| 7e5157f218 | |||
| cead85b165 | |||
| 25d4b9fc0c | |||
| 4cdfd64db0 | |||
| 70c2568af8 | |||
| 0683074b8b | |||
| 8f6c07afd6 | |||
| 3b2f0a633c | |||
| c920a8607c | |||
| eb8d8e84f2 | |||
| 619aa58fb3 | |||
| 7accdc4416 | |||
| ae8e5c4dbc | |||
| 454eb590ce | |||
| 9698c5e40a | |||
| 48680577a8 | |||
| a170e6454f | |||
| 25f90bc625 | |||
| c3f3cc085a | |||
| 9871e4f5d0 | |||
| 84229f1af9 | |||
| 3c3241f93c | |||
| 53dd2c1f1d | |||
| 592151bdb6 | |||
| e494d6e381 | |||
| 05b88c8add | |||
| 7a397c3d13 | |||
| bd5aed7b90 | |||
| 59c8dac851 | |||
| 41a2b7e58e | |||
| 1b86913c00 | |||
| 3e1e0b604f | |||
| 54602b55bb | |||
| 086b985383 | |||
| 25cc0d2447 | |||
| d1afe13a1c | |||
| 9974e0f458 | |||
| 7b32f59281 | |||
| 7389c79c52 | |||
| d5adf8b9f6 | |||
| 47de317b65 | |||
| 9e3252d1b0 | |||
| 2570c3410b | |||
| dc4240e702 | |||
| 997e9422eb | |||
| cb494ac416 | |||
| 078f886f39 | |||
| 15727f716a | |||
| 476898b1d4 | |||
| 1a78e3a5bc | |||
| 86e71c48f5 | |||
| 6ebb29cd22 | |||
| e2315e6a54 | |||
| 61f0c8aa66 | |||
| bf632fad2e | |||
| 8cebd985a2 | |||
| 6ff617a733 | |||
| 4d9956cf65 | |||
| ac3c936820 | |||
| 08fc32b0e1 | |||
| d8ad49d14d | |||
| 3debf75eb9 | |||
| 200b1c304b | |||
| 1b33bb3d1c | |||
| 33fdb2c489 | |||
| cfb6ddfcea | |||
| 8e1ed8bef3 | |||
| 30f22fbd78 | |||
| a155ab6deb | |||
| a4ebb33cd5 | |||
| 64fa1aa8cf | |||
| 120d6ec7d8 | |||
| 6291b59849 | |||
| a96e966911 | |||
| 69aa650203 | |||
| 8028cc764f | |||
| b3869a7833 | |||
| 912a98788e | |||
| f90e9dfab0 | |||
| c43c9458a9 | |||
| 90064a8cf0 | |||
| bacab20f4a | |||
| 7b995044b4 | |||
| e878dca3d7 | |||
| fad9ad2be4 | |||
| 15fe9c20df | |||
| 789ad8257a | |||
| 63a549f0e3 | |||
| d422d4e067 | |||
| 881c0f406b | |||
| ee84731153 | |||
| b62ecc6699 | |||
| 21f8b03483 | |||
| 03406c68d0 | |||
| d3883126bf | |||
| e38333e272 | |||
| 2eb2441f2b | |||
| 7409bba0d8 | |||
| 70940e768c | |||
| 37f93d5127 | |||
| 5517eebafe | |||
| bf357a0e2d | |||
| 02dbe3ef2e | |||
| bf2ceded96 | |||
| 97d1c6f02b | |||
| e0b600b651 | |||
| 3379429329 | |||
| bdca3372f9 | |||
| ca9cc692e4 | |||
| afe1faf214 | |||
| d610d091fe | |||
| 36d7732234 | |||
| ca48f7612a | |||
| 7d50cc40de | |||
| 934385ff55 | |||
| fb9a1a6129 | |||
| 3a60abbac0 | |||
| 8d8f78241d | |||
| 937492f6f5 | |||
| b94160df72 | |||
| 456ebaf423 | |||
| 2d27f53941 | |||
| b6f3160dbc | |||
| 77125f45cc | |||
| d882bbca85 | |||
| ac45307da6 | |||
| ea2b6e3608 | |||
| 3a39d110f0 | |||
| 6d39f2a4cb | |||
| f82736641f | |||
| 3d19313a7f | |||
| 17aae72583 | |||
| a634839678 | |||
| ac3693562b | |||
| 99149d9f2a | |||
| 2b6e17c1ef | |||
| 34421dcd49 | |||
| 48abf57218 | |||
| 867dcb0004 | |||
| c840268678 | |||
| f783b135c7 | |||
| a04fe1aa7e | |||
| d01a2dd651 | |||
| ec8fa56c95 | |||
| 91508c50b5 | |||
| f7b7b63e3f | |||
| bb720c87c1 | |||
| 228862fefa | |||
| 04d85b3c63 | |||
| 5f487e8c79 | |||
| 0dff432337 | |||
| 4295a86e55 | |||
| 3c8cc0b2b6 | |||
| 34df45da2f | |||
| a6068aae1d | |||
| 8ee1a09dc7 | |||
| f593dc8463 | |||
| 129f1bf3da | |||
| 304b43cb30 | |||
| 3a483be258 | |||
| 220d9e3d56 | |||
| fc473f1d11 | |||
| 22b68eefd8 | |||
| 6764a420b0 | |||
| 45df254fe7 | |||
| f3fc8b7072 | |||
| 1634e267b8 | |||
| 267b706544 | |||
| ed6ae7e0d2 | |||
| c71681b197 | |||
| 04b6eb509c | |||
| 7897d8a8ad |
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
- [ ] I have read this document: https://miniflux.app/opinionated.html#feature-request
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Improvement
|
||||
about: Do you have an idea to improve an existing feature?
|
||||
title: ''
|
||||
labels: improvements
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Installation Issue
|
||||
about: Do you need help to install Miniflux?
|
||||
title: ''
|
||||
labels: installation issue
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Question / Discussion
|
||||
about: Open discussions
|
||||
title: ''
|
||||
labels: question / discussion
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
@@ -0,0 +1,4 @@
|
||||
Do you follow the guidelines?
|
||||
|
||||
- [ ] I have tested my changes
|
||||
- [ ] I read this document: https://miniflux.app/faq.html#pull-request
|
||||
@@ -0,0 +1,53 @@
|
||||
name: CI Workflow
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
go-version: [1.16]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Run unit tests
|
||||
run: make test
|
||||
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:9.5
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Postgres client
|
||||
run: sudo apt-get install -y postgresql-client
|
||||
- name: Run integration tests
|
||||
run: make integration-test
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PGHOST: 127.0.0.1
|
||||
PGPASSWORD: postgres
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Docker
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
jobs:
|
||||
docker-images:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate Docker tag
|
||||
id: docker_tag
|
||||
run: |
|
||||
DOCKER_IMAGE=miniflux/miniflux
|
||||
DOCKER_VERSION=dev
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
DOCKER_VERSION=nightly
|
||||
TAGS="${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
|
||||
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
DOCKER_VERSION=${GITHUB_REF#refs/tags/}
|
||||
TAGS="${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},${DOCKER_IMAGE}:latest,ghcr.io/${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.CR_PAT }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.docker_tag.outputs.tags }}
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Linters
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
jshint:
|
||||
name: Javascript Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install jshint
|
||||
run: |
|
||||
sudo npm install -g jshint
|
||||
- name: Run jshint
|
||||
run: jshint ui/static/js/*.js
|
||||
|
||||
golangci:
|
||||
name: Golang Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
- uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
args: --skip-dirs tests --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
|
||||
skip-go-installation: true
|
||||
+3
-1
@@ -1,2 +1,4 @@
|
||||
miniflux-*
|
||||
miniflux
|
||||
miniflux
|
||||
*.rpm
|
||||
*.deb
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
notifications:
|
||||
email: false
|
||||
services:
|
||||
- postgresql
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
language: go
|
||||
go_import_path: "miniflux.app"
|
||||
go:
|
||||
- "1.11"
|
||||
before_install:
|
||||
- npm install -g jshint
|
||||
- go get -u golang.org/x/lint/golint
|
||||
script:
|
||||
- jshint ui/static/js/*.js
|
||||
- make lint
|
||||
- make test
|
||||
- make integration-test
|
||||
@@ -1,4 +1,428 @@
|
||||
Version 2.0.14 (Jan 13, 2018)
|
||||
Version 2.0.30 (May 7, 2021)
|
||||
----------------------------
|
||||
|
||||
* Security fix: any user can delete any feed (Regression introduced in commit 51fb949)
|
||||
* Fix password reset via CLI
|
||||
* Increase default batch size value
|
||||
* Handle RSS feed title with encoded Unicode entities
|
||||
* Show number of unread per category in category list instead of number of feeds
|
||||
* Bump github.com/lib/pq from 1.10.0 to 1.10.1
|
||||
* Filtering doesn't work when selecting from multiple found feeds
|
||||
* Bump github.com/tdewolff/minify/v2 from 2.9.15 to 2.9.16
|
||||
* Use an appropriate color for visited links on dark theme
|
||||
* Fix typo in reader/json/doc.go
|
||||
* Create SECURITY.md
|
||||
* Setup golangci-lint Github Action
|
||||
* Add per feed cookies option
|
||||
* Bump github.com/prometheus/client_golang from 1.9.0 to 1.10.0
|
||||
* Bump github.com/tdewolff/minify/v2 from 2.9.13 to 2.9.15
|
||||
|
||||
Version 2.0.29 (Mar 21, 2021)
|
||||
-----------------------------
|
||||
|
||||
* Miniflux requires at least Go 1.16 now
|
||||
* Improved support of Atom text constructs
|
||||
- Improve handling of CDATA in text elements
|
||||
- Omit XHTML root element because it should not be part of the content
|
||||
- Fix incorrect parsing of HTML elements
|
||||
* Handle RDF feed with HTML encoded entry title
|
||||
* Add Turkish language
|
||||
* Improve deletion of feeds with lots of entries
|
||||
* Add support of Systemd readiness notification using the sd_notify protocol
|
||||
* Remove feed_icons service worker cache because it's causing more problems than it solves (and HTTP cache seems faster)
|
||||
* Add basic PWA offline page
|
||||
- Add basic offline mode when using the service worker
|
||||
- Starting in Chrome 93, offline mode is going to be a requirement to install the PWA
|
||||
* Replace icon for "Add to home screen" button
|
||||
* Use SVG icons for "toast" notifications
|
||||
* Use SVG sprite for icons instead of inline elements
|
||||
* Reset scroll position on mark page as read
|
||||
* Add link to mark all feed entries as read
|
||||
* Make web app display mode configurable (The change is visible after reinstalling the web app)
|
||||
* Handle RSS feeds with CDATA in author item element
|
||||
* Add read time on the article page
|
||||
* Avoid showing a broken image when there is no feed icon
|
||||
* Add option to allow self-signed or invalid certificates
|
||||
* Add new config option to scrape YouTube's website to get video duration as read time (disabled by default)
|
||||
* Send full article content to Wallabag
|
||||
* Add more extensive health check support
|
||||
- Improve endpoint to test database connection
|
||||
- Add new cli argument: -healthcheck
|
||||
- Update Docker Compose examples
|
||||
* Update iframe "allow list" to support Bilibili videos
|
||||
* Remove completely generated files and use embed package to bundle JS/CSS/Images/Translations files into the final binary
|
||||
* Remove deprecated io/ioutil package
|
||||
* Show Postgres version in "About" page
|
||||
|
||||
Version 2.0.28 (Feb 15, 2021)
|
||||
-----------------------------
|
||||
|
||||
* Add HTTP header "Referrer-Policy: no-referrer"
|
||||
* Handle entry title with double encoded entities
|
||||
* Add Open Containers annotations to Docker image
|
||||
* Remove iframe inner HTML contents (iframe element never has fallback content)
|
||||
* Update date parser to fix another time zone issue
|
||||
* Update German translation for blocklist and keeplist
|
||||
* Validate Keep list and Block list rules syntax
|
||||
* Add support for IPv6 with zone index
|
||||
* Allow images with data URLs
|
||||
* Limit full-text search indexation to first 500K characters (tsvector has a size limit of 1MB)
|
||||
* Change PWA display mode to standalone
|
||||
* ETag value is not set correctly in HTTP client (regression)
|
||||
* Add database backed Let's Encrypt certificate cache
|
||||
* Add global option POLLING_PARSING_ERROR_LIMIT
|
||||
* Update systemd service file comments to use `systemctl edit` for editing
|
||||
* Update Go version to 1.15 in go.mod
|
||||
* Don't discard the "Fetch via Proxy" option
|
||||
* Update man page to show the default values
|
||||
* Add PostgreSQL indices
|
||||
* Add API endpoints to get feeds and entries of a category
|
||||
* Create feed query builder
|
||||
* Bump github.com/PuerkitoBio/goquery from 1.6.0 to 1.6.1
|
||||
* Show global options in the about page
|
||||
* Update man page to mention -1 can be used for CLEANUP_ARCHIVE_* options
|
||||
|
||||
Version 2.0.27 (Jan 9, 2021)
|
||||
----------------------------
|
||||
|
||||
* Add spellcheck="false" to input fields
|
||||
* Refactoring of entry, feed, category, and user validation
|
||||
* Avoid stripping tags for entry title
|
||||
* Add the possibility to subscribe to feeds with the Android Share menu
|
||||
* API improvements:
|
||||
- Change feed creation request to allow setting most fields via API
|
||||
- Allow regular users to change settings via API
|
||||
- Make user fields editable via API
|
||||
- Renaming non-existent category via API should return a 404
|
||||
* Update Systemd service file:
|
||||
- Add capability CAP_NET_BIND_SERVICE (allow the process to listen on privileged ports)
|
||||
- Enable a private /tmp for $CERT_CACHE (required when using Let's Encrypt)
|
||||
* Update read/star icons to SVGs
|
||||
* Add autocomplete="username" to HTML forms
|
||||
* Improve user mass delete to use fewer Goroutines
|
||||
* Use SQL transaction when creating user sessions and users
|
||||
* Remove extra column (HSTORE field) from users table and migrate key/value pairs to specific columns
|
||||
* Bump github.com/prometheus/client_golang from 1.8.0 to 1.9.0
|
||||
* Bump github.com/lib/pq from 1.8.0 to 1.9.0
|
||||
* Add styles for <abbr> HTML tag
|
||||
* Refactor SQL migrations:
|
||||
- Avoid embedding SQL files into binary
|
||||
- Allow more flexible changes by using Go functions
|
||||
* Add Server-Timing header to unread page
|
||||
* Show correct User Agent in input placeholders
|
||||
* Add autocomplete attribute to login form
|
||||
* Add Grafana dashboard in contrib folder
|
||||
|
||||
Version 2.0.26 (Dec 5, 2020)
|
||||
----------------------------
|
||||
|
||||
* Use created_at instead of published_at for archiving entries
|
||||
* Add created_at field for entries
|
||||
* Handle invalid feeds with relative URLs
|
||||
* Add API routes for "mark all as read"
|
||||
* Add support for setting a global default User-Agent
|
||||
* Add rewrite rule "replace" for custom search and replace
|
||||
* Calculate reading time during feed processing
|
||||
* Handle various invalid dates
|
||||
* systemd: keep /run writeable
|
||||
* debian package: add missing post-install script
|
||||
* Do not follow redirects when trying known feed URLs
|
||||
* Trim spaces around icon URLs
|
||||
* Reinstate EXPOSE instruction in Dockerfile
|
||||
* Update German and Portuguese translations
|
||||
|
||||
Version 2.0.25 (Nov 3, 2020)
|
||||
----------------------------
|
||||
|
||||
* Rename "original" link to be more explicit
|
||||
* Do not escape HTML for Atom 1.0 text content during parsing (Avoid HTML entities issues)
|
||||
* Do not use charset.NewReader if the body is a valid UTF-8 document
|
||||
* Restore the ability to use a proxy for all HTTP requests (see https://golang.org/pkg/net/http/#ProxyFromEnvironment)
|
||||
* Show Git commit in about page
|
||||
* Publish Docker images to GitHub Container Registry
|
||||
* Added few Docker Compose examples in contrib folder
|
||||
* Added Ansible Role + Playbook for Miniflux in contrib folder
|
||||
* Add rewrite rule to use noscript content for images rendered with Javascript
|
||||
* Bump github.com/prometheus/client_golang from 1.7.1 to 1.8.0
|
||||
* Update contributor link and Godoc badge for API client
|
||||
* Move Debian package builder to main repository
|
||||
* Move RPM build files to main repository
|
||||
* Add GitHub Action to generate Docker images
|
||||
* Build multi-platform images with Docker Buildx
|
||||
* Add keyboard shortcut to scroll current item to the top
|
||||
* Add feed filters (Keeplist and Blocklist)
|
||||
* Do not proxy image with a data URL
|
||||
* Bump github.com/PuerkitoBio/goquery from 1.5.1 to 1.6.0
|
||||
* Proxify articles crawled manually
|
||||
* Proxify images defined in srcset attribute
|
||||
* Remove plaintext Fever password from database
|
||||
* Add keyboard shortcut to jump to an item's feed page
|
||||
* Add option for swipe gesture on entries on mobile
|
||||
|
||||
Version 2.0.24 (Oct 3, 2020)
|
||||
----------------------------
|
||||
|
||||
* Add rewrite rule to fix Medium.com images
|
||||
* Update sanitizer to support responsive images:
|
||||
- Add support for picture HTML tag
|
||||
- Add support for srcset, media, and sizes attributes to img and source tags
|
||||
* Enhance man page formatting
|
||||
* Add Prometheus exporter
|
||||
* Remove dependency on global config options in HTTP client
|
||||
* API:
|
||||
- Avoid database lookup if empty credentials are provided
|
||||
- Add the possibility to filter entries by category ID
|
||||
- Add the possibility to filter entries by a list of statuses
|
||||
* Add Feed ID in worker error logs
|
||||
* Tweak default HTTP client transport timeout values to reduce the number of file descriptors
|
||||
* CSS tweaks and optimizations:
|
||||
- Prevent sub and sup from affecting line-height
|
||||
- Set touch-action on .items to prevent browser navigation
|
||||
- Move font-family specific CSS to the appropriate file
|
||||
- Update primary font-family for UI to be even more compatible with various operating systems
|
||||
- Make .entry-content font-weight variable depending on font-family used
|
||||
* Avoid Javascript minifier to break keyboard shortcuts
|
||||
* Rename service worker script to avoid being blocked by uBlock
|
||||
* Update date parser to handle Pacific Daylight Time in addition to Pacific Standard Time
|
||||
* Create index to speed up bookmark page
|
||||
* Do not try to update a duplicated feed after a refresh
|
||||
* Use a transaction to refresh and create entries
|
||||
* Speed up entries clean up with an index and a goroutine
|
||||
* Avoid the accumulation of enclosures by keeping only what is referenced in the feed
|
||||
* Add workarounds for parsing an invalid date
|
||||
* Archive older entries first
|
||||
* Update API client to support more filters
|
||||
* Avoid duplication between get feed entries and get entries API endpoints
|
||||
* Enable strict slash to avoid a page not found (404) when using a trailing slash in the URLs
|
||||
* Add a submit button to each section of the integration page
|
||||
* Reload page after making page as read when showing unread entries
|
||||
* Add option to archive unread entries
|
||||
* Add option to enable maintenance mode
|
||||
* Add HTTP proxy option for subscriptions
|
||||
* Make add_invidious_video rule applicable for different invidious instances
|
||||
* Fix reading time for jp, ko and zh languages
|
||||
* Update POLLING_SCHEDULER description in man page
|
||||
* Bump gorilla/mux from 1.7.4 to 1.8.0
|
||||
* Add link to mark a feed as read
|
||||
|
||||
Version 2.0.23 (Aug 15, 2020)
|
||||
-----------------------------
|
||||
|
||||
* Try known URLs when discovering subscriptions
|
||||
* Add workarounds to find YouTube channel feeds (YouTube doesn't expose RSS links anymore for new-style URLs)
|
||||
* Increase HTTP server timeout values
|
||||
* Use stdlib constants for HTTP methods instead of strings
|
||||
* Add support for RTL feed content
|
||||
* Ignore <media:title> to avoid overriding the default title if they are different
|
||||
* Add support for secret keys exposed as a file (useful for containerized environments)
|
||||
* Display recent entries first in search results
|
||||
* Do not archive shared items
|
||||
* Add option to change the number of entries per page
|
||||
* Add Brazilian Portuguese (pt_BR) translation
|
||||
* Add reading time for entries
|
||||
* Redirect to login page if CSRF token is expired
|
||||
* Fever API:
|
||||
- Use getEntryIDs instead of getEntries to reduce memory consumption
|
||||
- Fix max_id argument logic to follow the specs
|
||||
- Improve logging
|
||||
- Do not send articles to external services when unsaving an item
|
||||
- Create index to speed up API calls
|
||||
- Log client IP in middleware
|
||||
* API client: Do not return body for response with no content
|
||||
* REST API:
|
||||
- Delete users asynchronously (Deleting large users might lock the tables)
|
||||
- Add CORS support
|
||||
* Align entry actions to the left
|
||||
- Attempt to avoid awkward alignment on smartphone screens
|
||||
- Keep the read/star actions aligned to the left
|
||||
- Remove CSS flex to allow easier override with custom CSS
|
||||
* Upgrade Postgres client library
|
||||
* Upgrade CI checks to Go 1.15
|
||||
|
||||
Version 2.0.22 (Jun 19, 2020)
|
||||
-----------------------------
|
||||
|
||||
* Remove child-src CSP policy (deprecated)
|
||||
* Add /version endpoint
|
||||
* Add the ability to use custom css
|
||||
* Handle more invalid dates
|
||||
* Add CSS styles for textarea
|
||||
* Add index to speed up slow query
|
||||
* Speed up feed list page rendering
|
||||
* Add alternative scheduler based on the number of entries
|
||||
* Setup Dependabot on GitHub
|
||||
* Update Docker image to Alpine 3.12
|
||||
* Add feed option to ignore HTTP cache
|
||||
* Fix some Italian and German translations
|
||||
* Added scraper rule for RayWenderlich.com, TheOatmeal.com, financialsamurai.com, dilbert.com and turnoff.us
|
||||
* Replace link to categories by a link to the list of entries in "Category > Feeds" page
|
||||
* Change feed title to a link to the original website
|
||||
* Add icons to feeds and categories list
|
||||
* Update dependencies and remove vendor folder
|
||||
|
||||
Version 2.0.21 (Mar 28, 2020)
|
||||
-----------------------------
|
||||
|
||||
* Add SVG icons to entry actions
|
||||
* Add support for Invidious
|
||||
- Embed Invidious player for invidio.us feeds
|
||||
- Add new rewrite rule to use Invidious player for Youtube feeds
|
||||
* Check during application startup if the database schema is up to date
|
||||
* Change default theme for public pages to "System Serif"
|
||||
* Add feature to share an article (create a public link of a feed entry)
|
||||
* Fix SQL injection in full-text search rank ordering
|
||||
* Add generic OpenID Connect provider (OAuth2)
|
||||
* Use more secure TLS configuration for autocert server (increase SSL Labs score from B to A+)
|
||||
* Add feature to create per-application API Keys
|
||||
* Add Go 1.14 to GitHub Actions
|
||||
* Add scraper rule for wdwnt.com
|
||||
* Add API client function to refresh all feeds
|
||||
* Add API endpoint to refresh all feeds
|
||||
* Add Auth Proxy authentication
|
||||
* Use rel=prev/next on pagination links
|
||||
|
||||
Version 2.0.20 (Feb 15, 2020)
|
||||
-----------------------------
|
||||
|
||||
* Add Japanese translation
|
||||
* History: show entries in the order in which they were read
|
||||
* Add button to add to Home screen
|
||||
* Ignore enclosures without URL
|
||||
* Correct spelling of "toggle"
|
||||
* List view: align information to the left side, and the actionable buttons to the right
|
||||
* Redirect to /unread when getting a 404 for an unread expired entry
|
||||
* Do not advance to the next item when using the 'v' shortcut on the list of starred items
|
||||
* Wrap around when navigating with keyboard shortcuts on a list view
|
||||
* Remove unused Feed.Entries and Entry.Category from API client
|
||||
* Add comments link keyboard shortcut
|
||||
* Allow application/xhtml+xml links as comments URL in Atom replies
|
||||
* Allow only absolute URLs in comments URL
|
||||
* Use internal XML workarounds to detect feed format
|
||||
* Make menu consistent across feed pages
|
||||
* Make sure external URLs are not encoded incorrectly by Go template engine
|
||||
* Make sure whitelisted URI schemes are handled properly by the sanitizer
|
||||
* Use white background for favicon (Improve legibility when using a dark theme)
|
||||
* Remove dependency on Sed to build Docker images
|
||||
* Normalize URL query string before executing HTTP requests
|
||||
* Improve Dublin Core support for RDF feeds
|
||||
* Improve Podcast support (iTunes and Google Play feeds)
|
||||
* Add support for Atom 0.3
|
||||
* Add support for Atom "replies" link relation
|
||||
* Return outer HTML when scraping elements
|
||||
* Update scraper rule for "Le Monde"
|
||||
* Filter valid XML characters for UTF-8 XML documents before decoding
|
||||
* Trim spaces for RDF entry links
|
||||
|
||||
Version 2.0.19 (Dec 1, 2019)
|
||||
----------------------------
|
||||
|
||||
* Add shortcut "V" to open original link in current tab
|
||||
* Add the possibility to add rules during feed creation
|
||||
* Wrap attachments into <details> disclosure element
|
||||
* Show attachment size on entry page
|
||||
* Add support of RSS Media elements (group, description, peer link, and thumbnails)
|
||||
* Add rewrite functions: convert_text_link and nl2br
|
||||
* Add scraper rule for openingsource.org
|
||||
* Add Makefile target to build only amd64 Docker image
|
||||
* Make sure to remove integration settings when removing a user
|
||||
* Add API parameter to filter entries by category
|
||||
* Display list of feeds per category
|
||||
* Show the number of read and unread entries for each feed
|
||||
* Make sure settings menu is consistent
|
||||
* Remove fixed table-layout for entry content
|
||||
* Update autocert lib because ACME v1 is EOL
|
||||
* Do not lighten blockquote font color
|
||||
* Update de_DE translation
|
||||
* Send a response when changing status of removed entries in Fever API
|
||||
* Add meta tag to disable Google Translate
|
||||
* Improve storage module
|
||||
* Improve XML decoder to remove illegal characters
|
||||
* Compare Fever token case-insensitively
|
||||
* Make sure integration tests are marked as failed in Github Actions
|
||||
* Add new formats to date parser
|
||||
* Add notification message when using keyboard shortcuts: f, s, and m.
|
||||
* Avoid keyboard shortcuts to conflict with Firefox’s "Find as you type" feature
|
||||
|
||||
Version 2.0.18 (Sep 25, 2019)
|
||||
-----------------------------
|
||||
|
||||
* Add Docker image variant for arm32v7
|
||||
* Add theme variants
|
||||
- Use CSS variables instead of inherence
|
||||
- Rename default theme to "Light - Serif"
|
||||
- Rename Black theme to "Dark - Serif"
|
||||
- Rename "Sans-Serif" theme to "Light - Sans Serif"
|
||||
- Add "System" theme that use system preferences: Dark or Light
|
||||
- Add Serif and Sans-Serif variants for each color theme
|
||||
* Avoid constraint error when having duplicate entries during feed creation
|
||||
* Disable strict XML parsing
|
||||
* Ignore invalid content type
|
||||
* Update man page
|
||||
* Replace Travis by GitHub Actions
|
||||
* Rename cleanup config variables and deprecate old ones
|
||||
- CLEANUP_FREQUENCY_HOURS instead of CLEANUP_FREQUENCY
|
||||
- CLEANUP_ARCHIVE_READ_DAYS instead of ARCHIVE_READ_DAYS
|
||||
* Make configurable the number of days to remove old sessions
|
||||
* Add native lazy loading for images and iframes
|
||||
* Do not buffer responses in the image proxy
|
||||
* Update dependencies
|
||||
* Add Go 1.13 to test matrix
|
||||
* Replace link border by outline to avoid slight content shift
|
||||
* New rewrite function: add_mailto_subject
|
||||
* Import OPML from URL
|
||||
* Fix HTML injection in addImageTitle
|
||||
* Accept HTML entities when parsing XML
|
||||
|
||||
Version 2.0.17 (Aug 3, 2019)
|
||||
----------------------------
|
||||
|
||||
* Update Docker image to Alpine Linux 3.10.1
|
||||
* Pass auth header to manifest request (crossorigin attribute)
|
||||
* Sort feed categories before serialization
|
||||
* Fix syntax errors in man page
|
||||
* Add .search margin-right
|
||||
* Ask for confirmation before flushing history, marking page as read, and mark all as read
|
||||
* Add option to disable feeds
|
||||
|
||||
Version 2.0.16 (Jun 8, 2019)
|
||||
----------------------------
|
||||
|
||||
* Add option to toggle date/time in log messages
|
||||
* Add optional config file parser in addition to environment variables
|
||||
* Make HTTP Client timeout and max body size configurable
|
||||
* Refactor config package:
|
||||
- Parse configuration only once during startup time
|
||||
- Store configuration values in a global variable
|
||||
* Flip behavior of j and k keyboard shortcuts
|
||||
* Bump Postgresql client library to v1.1.1 to bring in SCRAM-SHA-256 authentication
|
||||
* Add option to enable/disable keyboard shortcuts
|
||||
* Add missing translation
|
||||
* Improve page reload when showing unread/all entries:
|
||||
- Show only unread entries = refresh current page
|
||||
- Show all entries = go to next page
|
||||
* Always display feed entries even when there is a feed error
|
||||
* Use loading label instead of saving when submitting login form
|
||||
* Add OPML v1 support during importation
|
||||
* Add 'allow-popups' to iframe sandbox permissions
|
||||
|
||||
Version 2.0.15 (Mar 16, 2019)
|
||||
-----------------------------
|
||||
|
||||
* Move Dockerfile to main repo
|
||||
* Change location of the binary from /usr/local/bin to /usr/bin in Docker image
|
||||
* Add double tap detection for next/previous page navigation
|
||||
* Allow users to disable auto-remove
|
||||
* Make parser compatible with Go 1.12
|
||||
* Add Golang 1.12 to CI
|
||||
* Use feed ID instead of user ID to check entry URLs presence
|
||||
* Fix typo in stylesheet
|
||||
* Sort search results by relevance
|
||||
* Use preferably the published date for Atom feeds
|
||||
* Add Spanish translation
|
||||
* Rename session cookies
|
||||
* Handle the case when application session is expired and not user session
|
||||
|
||||
Version 2.0.14 (Jan 13, 2019)
|
||||
-----------------------------
|
||||
|
||||
* Only attempt to change password if the confirmation field is filled in (Firefox)
|
||||
|
||||
@@ -1,113 +1,163 @@
|
||||
APP := miniflux
|
||||
VERSION := $(shell git rev-parse --short HEAD)
|
||||
BUILD_DATE := `date +%FT%T%z`
|
||||
LD_FLAGS := "-s -w -X 'miniflux.app/version.Version=$(VERSION)' -X 'miniflux.app/version.BuildDate=$(BUILD_DATE)'"
|
||||
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
||||
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
|
||||
APP := miniflux
|
||||
DOCKER_IMAGE := miniflux/miniflux
|
||||
VERSION := $(shell git describe --tags --abbrev=0)
|
||||
COMMIT := $(shell git rev-parse --short HEAD)
|
||||
BUILD_DATE := `date +%FT%T%z`
|
||||
LD_FLAGS := "-s -w -X 'miniflux.app/version.Version=$(VERSION)' -X 'miniflux.app/version.Commit=$(COMMIT)' -X 'miniflux.app/version.BuildDate=$(BUILD_DATE)'"
|
||||
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
||||
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
|
||||
DEB_IMG_ARCH := amd64
|
||||
|
||||
export GO111MODULE=on
|
||||
export PGPASSWORD := postgres
|
||||
|
||||
.PHONY: generate
|
||||
.PHONY: miniflux
|
||||
.PHONY: linux-amd64
|
||||
.PHONY: linux-armv8
|
||||
.PHONY: linux-armv7
|
||||
.PHONY: linux-armv6
|
||||
.PHONY: linux-armv5
|
||||
.PHONY: darwin-amd64
|
||||
.PHONY: freebsd-amd64
|
||||
.PHONY: openbsd-amd64
|
||||
.PHONY: windows-amd64
|
||||
.PHONY: netbsd-amd64
|
||||
.PHONY: linux-x86
|
||||
.PHONY: darwin-x86
|
||||
.PHONY: freebsd-x86
|
||||
.PHONY: netbsd-x86
|
||||
.PHONY: openbsd-x86
|
||||
.PHONY: windows-x86
|
||||
.PHONY: build run clean test lint integration-test clean-integration-test
|
||||
.PHONY: \
|
||||
miniflux \
|
||||
linux-amd64 \
|
||||
linux-arm64 \
|
||||
linux-armv7 \
|
||||
linux-armv6 \
|
||||
linux-armv5 \
|
||||
linux-x86 \
|
||||
darwin-amd64 \
|
||||
darwin-arm64 \
|
||||
freebsd-amd64 \
|
||||
freebsd-x86 \
|
||||
openbsd-amd64 \
|
||||
openbsd-x86 \
|
||||
netbsd-x86 \
|
||||
netbsd-amd64 \
|
||||
windows-amd64 \
|
||||
windows-x86 \
|
||||
build \
|
||||
run \
|
||||
clean \
|
||||
test \
|
||||
lint \
|
||||
integration-test \
|
||||
clean-integration-test \
|
||||
docker-image \
|
||||
docker-images \
|
||||
rpm \
|
||||
debian \
|
||||
debian-packages
|
||||
|
||||
generate:
|
||||
@ go generate -mod=vendor
|
||||
miniflux:
|
||||
@ go build -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
|
||||
miniflux: generate
|
||||
@ go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
linux-amd64:
|
||||
@ GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-amd64 main.go
|
||||
|
||||
linux-amd64: generate
|
||||
@ GOOS=linux GOARCH=amd64 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-linux-amd64 main.go
|
||||
linux-arm64:
|
||||
@ GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-arm64 main.go
|
||||
|
||||
linux-armv8: generate
|
||||
@ GOOS=linux GOARCH=arm64 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv8 main.go
|
||||
linux-armv7:
|
||||
@ GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv7 main.go
|
||||
|
||||
linux-armv7: generate
|
||||
@ GOOS=linux GOARCH=arm GOARM=7 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv7 main.go
|
||||
linux-armv6:
|
||||
@ GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv6 main.go
|
||||
|
||||
linux-armv6: generate
|
||||
@ GOOS=linux GOARCH=arm GOARM=6 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv6 main.go
|
||||
linux-armv5:
|
||||
@ GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv5 main.go
|
||||
|
||||
linux-armv5: generate
|
||||
@ GOOS=linux GOARCH=arm GOARM=5 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv5 main.go
|
||||
darwin-amd64:
|
||||
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-darwin-amd64 main.go
|
||||
|
||||
darwin-amd64: generate
|
||||
@ GOOS=darwin GOARCH=amd64 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-darwin-amd64 main.go
|
||||
darwin-arm64:
|
||||
@ GOOS=darwin GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-darwin-arm64 main.go
|
||||
|
||||
freebsd-amd64: generate
|
||||
@ GOOS=freebsd GOARCH=amd64 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-amd64 main.go
|
||||
freebsd-amd64:
|
||||
@ GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-amd64 main.go
|
||||
|
||||
openbsd-amd64: generate
|
||||
@ GOOS=openbsd GOARCH=amd64 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-openbsd-amd64 main.go
|
||||
openbsd-amd64:
|
||||
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-openbsd-amd64 main.go
|
||||
|
||||
windows-amd64: generate
|
||||
@ GOOS=windows GOARCH=amd64 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-windows-amd64 main.go
|
||||
windows-amd64:
|
||||
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-amd64 main.go
|
||||
|
||||
build: linux-amd64 linux-armv8 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 freebsd-amd64 openbsd-amd64 windows-amd64
|
||||
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64 windows-amd64
|
||||
|
||||
# NOTE: unsupported targets
|
||||
netbsd-amd64: generate
|
||||
@ GOOS=netbsd GOARCH=amd64 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-amd64 main.go
|
||||
|
||||
linux-x86: generate
|
||||
@ GOOS=linux GOARCH=386 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-linux-x86 main.go
|
||||
netbsd-amd64:
|
||||
@ GOOS=netbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-amd64 main.go
|
||||
|
||||
darwin-x86: generate
|
||||
@ GOOS=darwin GOARCH=386 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-darwin-x86 main.go
|
||||
linux-x86:
|
||||
@ GOOS=linux GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-x86 main.go
|
||||
|
||||
freebsd-x86: generate
|
||||
@ GOOS=freebsd GOARCH=386 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
|
||||
freebsd-x86:
|
||||
@ GOOS=freebsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
|
||||
|
||||
netbsd-x86: generate
|
||||
@ GOOS=netbsd GOARCH=386 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-x86 main.go
|
||||
netbsd-x86:
|
||||
@ GOOS=netbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-x86 main.go
|
||||
|
||||
openbsd-x86: generate
|
||||
@ GOOS=openbsd GOARCH=386 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
|
||||
openbsd-x86:
|
||||
@ GOOS=openbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
|
||||
|
||||
windows-x86: generate
|
||||
@ GOOS=windows GOARCH=386 go build -mod=vendor -ldflags=$(LD_FLAGS) -o $(APP)-windows-x86 main.go
|
||||
windows-x86:
|
||||
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-x86 main.go
|
||||
|
||||
run: generate
|
||||
@ go run -mod=vendor main.go -debug
|
||||
run:
|
||||
@ LOG_DATE_TIME=1 go run main.go -debug
|
||||
|
||||
clean:
|
||||
@ rm -f $(APP)-* $(APP)
|
||||
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb
|
||||
|
||||
test:
|
||||
go test -mod=vendor -cover -race -count=1 ./...
|
||||
go test -cover -race -count=1 ./...
|
||||
|
||||
lint:
|
||||
@ golint -set_exit_status ${PKG_LIST}
|
||||
golint -set_exit_status ${PKG_LIST}
|
||||
|
||||
integration-test:
|
||||
psql -U postgres -c 'drop database if exists miniflux_test;'
|
||||
psql -U postgres -c 'create database miniflux_test;'
|
||||
DATABASE_URL=$(DB_URL) go run -mod=vendor main.go -migrate
|
||||
DATABASE_URL=$(DB_URL) ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run -mod=vendor main.go -create-admin
|
||||
go build -mod=vendor -o miniflux-test main.go
|
||||
DATABASE_URL=$(DB_URL) ./miniflux-test -debug >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||
while ! echo exit | nc localhost 8080; do sleep 1; done >/dev/null
|
||||
go test -mod=vendor -v -tags=integration -count=1 miniflux.app/tests || cat /tmp/miniflux.log
|
||||
go build -o miniflux-test main.go
|
||||
|
||||
DATABASE_URL=$(DB_URL) \
|
||||
ADMIN_USERNAME=admin \
|
||||
ADMIN_PASSWORD=test123 \
|
||||
CREATE_ADMIN=1 \
|
||||
RUN_MIGRATIONS=1 \
|
||||
DEBUG=1 \
|
||||
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||
|
||||
while ! nc -z localhost 8080; do sleep 1; done
|
||||
go test -v -tags=integration -count=1 miniflux.app/tests
|
||||
|
||||
clean-integration-test:
|
||||
@ kill -9 `cat /tmp/miniflux.pid`
|
||||
@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
|
||||
@ rm miniflux-test
|
||||
@ psql -U postgres -c 'drop database if exists miniflux_test;'
|
||||
|
||||
docker-image:
|
||||
docker build -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/Dockerfile .
|
||||
|
||||
docker-images:
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 \
|
||||
--file packaging/docker/Dockerfile \
|
||||
--tag $(DOCKER_IMAGE):$(VERSION) \
|
||||
--push .
|
||||
|
||||
rpm: clean
|
||||
@ docker build \
|
||||
-t miniflux-rpm-builder \
|
||||
-f packaging/rpm/Dockerfile \
|
||||
.
|
||||
@ docker run --rm \
|
||||
-v ${PWD}:/root/rpmbuild/RPMS/x86_64 miniflux-rpm-builder \
|
||||
rpmbuild -bb --define "_miniflux_version $(VERSION)" /root/rpmbuild/SPECS/miniflux.spec
|
||||
|
||||
debian:
|
||||
@ docker build \
|
||||
--build-arg BASE_IMAGE_ARCH=$(DEB_IMG_ARCH) \
|
||||
-t $(DEB_IMG_ARCH)/miniflux-deb-builder \
|
||||
-f packaging/debian/Dockerfile \
|
||||
.
|
||||
@ docker run --rm \
|
||||
-v ${PWD}:/pkg $(DEB_IMG_ARCH)/miniflux-deb-builder
|
||||
|
||||
debian-packages: clean
|
||||
$(MAKE) debian DEB_IMG_ARCH=amd64
|
||||
$(MAKE) debian DEB_IMG_ARCH=arm64v8
|
||||
$(MAKE) debian DEB_IMG_ARCH=arm32v7
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
Miniflux 2
|
||||
==========
|
||||
[](https://travis-ci.org/miniflux/miniflux)
|
||||
[](https://godoc.org/miniflux.app)
|
||||
[](https://docs.miniflux.app/)
|
||||
|
||||
Miniflux is a minimalist and opinionated feed reader:
|
||||
|
||||
@@ -21,39 +18,37 @@ Official website: <https://miniflux.app>
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The Miniflux documentation is available here: <https://docs.miniflux.app/> ([Man page](https://miniflux.app/miniflux.1.html))
|
||||
The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man page](https://miniflux.app/miniflux.1.html))
|
||||
|
||||
- [Opinionated?](https://docs.miniflux.app/en/latest/opinionated.html)
|
||||
- [Features](https://docs.miniflux.app/en/latest/features.html)
|
||||
- [Requirements](https://docs.miniflux.app/en/latest/requirements.html)
|
||||
- [Installation Instructions](https://docs.miniflux.app/en/latest/installation.html)
|
||||
- [Installation Tutorials](https://docs.miniflux.app/en/latest/tutorials.html)
|
||||
- [Upgrading to a New Version](https://docs.miniflux.app/en/latest/upgrade.html)
|
||||
- [Configuration](https://docs.miniflux.app/en/latest/configuration.html)
|
||||
- [Command Line Usage](https://docs.miniflux.app/en/latest/cli.html)
|
||||
- [User Interface Usage](https://docs.miniflux.app/en/latest/usage.html)
|
||||
- [Keyboard Shortcuts](https://docs.miniflux.app/en/latest/keyboard_shortcuts.html)
|
||||
- [Integration with External Services](https://docs.miniflux.app/en/latest/integration.html)
|
||||
- [Scraper Rules](https://docs.miniflux.app/en/latest/scraper_rules.html)
|
||||
- [Rewrite Rules](https://docs.miniflux.app/en/latest/rewrite_rules.html)
|
||||
- [REST API](https://docs.miniflux.app/en/latest/api.html)
|
||||
- [Development](https://docs.miniflux.app/en/latest/development.html)
|
||||
- [Internationalization](https://docs.miniflux.app/en/latest/i18n.html)
|
||||
- [Frequently Asked Questions](https://docs.miniflux.app/en/latest/faq.html)
|
||||
- [Opinionated?](https://miniflux.app/opinionated.html)
|
||||
- [Features](https://miniflux.app/features.html)
|
||||
- [Requirements](https://miniflux.app/docs/requirements.html)
|
||||
- [Installation Instructions](https://miniflux.app/docs/installation.html)
|
||||
- [Upgrading to a New Version](https://miniflux.app/docs/upgrade.html)
|
||||
- [Configuration](https://miniflux.app/docs/configuration.html)
|
||||
- [Command Line Usage](https://miniflux.app/docs/cli.html)
|
||||
- [User Interface Usage](https://miniflux.app/docs/ui.html)
|
||||
- [Keyboard Shortcuts](https://miniflux.app/docs/keyboard_shortcuts.html)
|
||||
- [Integration with External Services](https://miniflux.app/docs/services.html)
|
||||
- [Rewrite and Scraper Rules](https://miniflux.app/docs/rules.html)
|
||||
- [API Reference](https://miniflux.app/docs/api.html)
|
||||
- [Development](https://miniflux.app/docs/development.html)
|
||||
- [Internationalization](https://miniflux.app/docs/i18n.html)
|
||||
- [Frequently Asked Questions](https://miniflux.app/faq.html)
|
||||
|
||||
Screenshots
|
||||
-----------
|
||||
|
||||
Default theme:
|
||||
|
||||

|
||||

|
||||
|
||||
Dark theme when using keyboard navigation:
|
||||
|
||||

|
||||

|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
- Authors: Frédéric Guillot - [List of contributors](https://github.com/miniflux/miniflux/graphs/contributors)
|
||||
- Authors: Frédéric Guillot - [List of contributors](https://github.com/miniflux/v2/graphs/contributors)
|
||||
- Distributed under Apache 2.0 License
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest stable version is supported.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Send an email to `security AT miniflux DOT net` will all the steps to reproduce the problem.
|
||||
+49
-31
@@ -5,43 +5,61 @@
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"miniflux.app/reader/feed"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/worker"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
store *storage.Storage
|
||||
pool *worker.Pool
|
||||
}
|
||||
|
||||
// Serve declares API routes for the application.
|
||||
func Serve(router *mux.Router, store *storage.Storage, feedHandler *feed.Handler) {
|
||||
handler := &handler{store, feedHandler}
|
||||
func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
||||
handler := &handler{store, pool}
|
||||
|
||||
sr := router.PathPrefix("/v1").Subrouter()
|
||||
sr.Use(newMiddleware(store).serve)
|
||||
sr.HandleFunc("/users", handler.createUser).Methods("POST")
|
||||
sr.HandleFunc("/users", handler.users).Methods("GET")
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}", handler.userByID).Methods("GET")
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}", handler.updateUser).Methods("PUT")
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}", handler.removeUser).Methods("DELETE")
|
||||
sr.HandleFunc("/users/{username}", handler.userByUsername).Methods("GET")
|
||||
sr.HandleFunc("/me", handler.currentUser).Methods("GET")
|
||||
sr.HandleFunc("/categories", handler.createCategory).Methods("POST")
|
||||
sr.HandleFunc("/categories", handler.getCategories).Methods("GET")
|
||||
sr.HandleFunc("/categories/{categoryID}", handler.updateCategory).Methods("PUT")
|
||||
sr.HandleFunc("/categories/{categoryID}", handler.removeCategory).Methods("DELETE")
|
||||
sr.HandleFunc("/discover", handler.getSubscriptions).Methods("POST")
|
||||
sr.HandleFunc("/feeds", handler.createFeed).Methods("POST")
|
||||
sr.HandleFunc("/feeds", handler.getFeeds).Methods("GET")
|
||||
sr.HandleFunc("/feeds/{feedID}/refresh", handler.refreshFeed).Methods("PUT")
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.getFeed).Methods("GET")
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.updateFeed).Methods("PUT")
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.removeFeed).Methods("DELETE")
|
||||
sr.HandleFunc("/feeds/{feedID}/icon", handler.feedIcon).Methods("GET")
|
||||
sr.HandleFunc("/export", handler.exportFeeds).Methods("GET")
|
||||
sr.HandleFunc("/import", handler.importFeeds).Methods("POST")
|
||||
sr.HandleFunc("/feeds/{feedID}/entries", handler.getFeedEntries).Methods("GET")
|
||||
sr.HandleFunc("/feeds/{feedID}/entries/{entryID}", handler.getFeedEntry).Methods("GET")
|
||||
sr.HandleFunc("/entries", handler.getEntries).Methods("GET")
|
||||
sr.HandleFunc("/entries", handler.setEntryStatus).Methods("PUT")
|
||||
sr.HandleFunc("/entries/{entryID}", handler.getEntry).Methods("GET")
|
||||
sr.HandleFunc("/entries/{entryID}/bookmark", handler.toggleBookmark).Methods("PUT")
|
||||
middleware := newMiddleware(store)
|
||||
sr.Use(middleware.handleCORS)
|
||||
sr.Use(middleware.apiKeyAuth)
|
||||
sr.Use(middleware.basicAuth)
|
||||
sr.Methods(http.MethodOptions)
|
||||
sr.HandleFunc("/users", handler.createUser).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/users", handler.users).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}", handler.userByID).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}", handler.updateUser).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}", handler.removeUser).Methods(http.MethodDelete)
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}/mark-all-as-read", handler.markUserAsRead).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/users/{username}", handler.userByUsername).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/me", handler.currentUser).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/categories", handler.createCategory).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/categories", handler.getCategories).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/categories/{categoryID}", handler.updateCategory).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/categories/{categoryID}", handler.removeCategory).Methods(http.MethodDelete)
|
||||
sr.HandleFunc("/categories/{categoryID}/mark-all-as-read", handler.markCategoryAsRead).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/categories/{categoryID}/feeds", handler.getCategoryFeeds).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/categories/{categoryID}/entries", handler.getCategoryEntries).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/categories/{categoryID}/entries/{entryID}", handler.getCategoryEntry).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/discover", handler.discoverSubscriptions).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds", handler.createFeed).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds", handler.getFeeds).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/refresh", handler.refreshAllFeeds).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/feeds/{feedID}/refresh", handler.refreshFeed).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.getFeed).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.updateFeed).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.removeFeed).Methods(http.MethodDelete)
|
||||
sr.HandleFunc("/feeds/{feedID}/icon", handler.feedIcon).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}/mark-all-as-read", handler.markFeedAsRead).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/export", handler.exportFeeds).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/import", handler.importFeeds).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds/{feedID}/entries", handler.getFeedEntries).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}/entries/{entryID}", handler.getFeedEntry).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries", handler.getEntries).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries", handler.setEntryStatus).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/entries/{entryID}", handler.getEntry).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries/{entryID}/bookmark", handler.toggleBookmark).Methods(http.MethodPut)
|
||||
}
|
||||
|
||||
+51
-18
@@ -5,33 +5,32 @@
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func (h *handler) createCategory(w http.ResponseWriter, r *http.Request) {
|
||||
category, err := decodeCategoryPayload(r.Body)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.UserID(r)
|
||||
category.UserID = userID
|
||||
if err := category.ValidateCategoryCreation(); err != nil {
|
||||
|
||||
var categoryRequest model.CategoryRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if c, err := h.store.CategoryByTitle(userID, category.Title); err != nil || c != nil {
|
||||
json.BadRequest(w, r, errors.New("This category already exists"))
|
||||
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.CreateCategory(category); err != nil {
|
||||
category, err := h.store.CreateCategory(userID, &categoryRequest)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
@@ -40,21 +39,32 @@ func (h *handler) createCategory(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
|
||||
category, err := decodeCategoryPayload(r.Body)
|
||||
category, err := h.store.Category(userID, categoryID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
var categoryRequest model.CategoryRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
category.UserID = request.UserID(r)
|
||||
category.ID = categoryID
|
||||
if err := category.ValidateCategoryModification(); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
categoryRequest.Patch(category)
|
||||
err = h.store.UpdateCategory(category)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
@@ -64,6 +74,29 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
json.Created(w, r, category)
|
||||
}
|
||||
|
||||
func (h *handler) markCategoryAsRead(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
|
||||
category, err := h.store.Category(userID, categoryID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.store.MarkCategoryAsRead(userID, categoryID, time.Now()); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
func (h *handler) getCategories(w http.ResponseWriter, r *http.Request) {
|
||||
categories, err := h.store.Categories(request.UserID(r))
|
||||
if err != nil {
|
||||
@@ -78,7 +111,7 @@ func (h *handler) removeCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
|
||||
if !h.store.CategoryExists(userID, categoryID) {
|
||||
if !h.store.CategoryIDExists(userID, categoryID) {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package api implements API endpoints for the application.
|
||||
|
||||
*/
|
||||
package api // import "miniflux.app/api"
|
||||
+77
-91
@@ -5,6 +5,7 @@
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
json_parser "encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -13,8 +14,24 @@ import (
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b *storage.EntryQueryBuilder) {
|
||||
entry, err := b.GetEntry()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, entry)
|
||||
}
|
||||
|
||||
func (h *handler) getFeedEntry(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
@@ -23,18 +40,18 @@ func (h *handler) getFeedEntry(w http.ResponseWriter, r *http.Request) {
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithEntryID(entryID)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
getEntryFromBuilder(w, r, builder)
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
func (h *handler) getCategoryEntry(w http.ResponseWriter, r *http.Request) {
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
|
||||
json.OK(w, r, entry)
|
||||
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
|
||||
builder.WithCategoryID(categoryID)
|
||||
builder.WithEntryID(entryID)
|
||||
|
||||
getEntryFromBuilder(w, r, builder)
|
||||
}
|
||||
|
||||
func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -42,104 +59,68 @@ func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) {
|
||||
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
|
||||
builder.WithEntryID(entryID)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, entry)
|
||||
getEntryFromBuilder(w, r, builder)
|
||||
}
|
||||
|
||||
func (h *handler) getFeedEntries(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
h.findEntries(w, r, feedID, 0)
|
||||
}
|
||||
|
||||
status := request.QueryStringParam(r, "status", "")
|
||||
if status != "" {
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
order := request.QueryStringParam(r, "order", model.DefaultSortingOrder)
|
||||
if err := model.ValidateEntryOrder(order); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
direction := request.QueryStringParam(r, "direction", model.DefaultSortingDirection)
|
||||
if err := model.ValidateDirection(direction); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
limit := request.QueryIntParam(r, "limit", 100)
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
if err := model.ValidateRange(offset, limit); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithStatus(status)
|
||||
builder.WithOrder(order)
|
||||
builder.WithDirection(direction)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(limit)
|
||||
configureFilters(builder, r)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, &entriesResponse{Total: count, Entries: entries})
|
||||
func (h *handler) getCategoryEntries(w http.ResponseWriter, r *http.Request) {
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
h.findEntries(w, r, 0, categoryID)
|
||||
}
|
||||
|
||||
func (h *handler) getEntries(w http.ResponseWriter, r *http.Request) {
|
||||
status := request.QueryStringParam(r, "status", "")
|
||||
if status != "" {
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
h.findEntries(w, r, 0, 0)
|
||||
}
|
||||
|
||||
func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int64, categoryID int64) {
|
||||
statuses := request.QueryStringParamList(r, "status")
|
||||
for _, status := range statuses {
|
||||
if err := validator.ValidateEntryStatus(status); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
order := request.QueryStringParam(r, "order", model.DefaultSortingOrder)
|
||||
if err := model.ValidateEntryOrder(order); err != nil {
|
||||
if err := validator.ValidateEntryOrder(order); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
direction := request.QueryStringParam(r, "direction", model.DefaultSortingDirection)
|
||||
if err := model.ValidateDirection(direction); err != nil {
|
||||
if err := validator.ValidateDirection(direction); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
limit := request.QueryIntParam(r, "limit", 100)
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
if err := model.ValidateRange(offset, limit); err != nil {
|
||||
if err := validator.ValidateRange(offset, limit); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
|
||||
builder.WithStatus(status)
|
||||
userID := request.UserID(r)
|
||||
categoryID = request.QueryInt64Param(r, "category_id", categoryID)
|
||||
if categoryID > 0 && !h.store.CategoryIDExists(userID, categoryID) {
|
||||
json.BadRequest(w, r, errors.New("Invalid category ID"))
|
||||
return
|
||||
}
|
||||
|
||||
feedID = request.QueryInt64Param(r, "feed_id", feedID)
|
||||
if feedID > 0 && !h.store.FeedExists(userID, feedID) {
|
||||
json.BadRequest(w, r, errors.New("Invalid feed ID"))
|
||||
return
|
||||
}
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithCategoryID(categoryID)
|
||||
builder.WithStatuses(statuses)
|
||||
builder.WithOrder(order)
|
||||
builder.WithDirection(direction)
|
||||
builder.WithOffset(offset)
|
||||
@@ -162,18 +143,18 @@ func (h *handler) getEntries(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *handler) setEntryStatus(w http.ResponseWriter, r *http.Request) {
|
||||
entryIDs, status, err := decodeEntryStatusPayload(r.Body)
|
||||
if err != nil {
|
||||
json.BadRequest(w , r, errors.New("Invalid JSON payload"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
var entriesStatusUpdateRequest model.EntriesStatusUpdateRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&entriesStatusUpdateRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.SetEntriesStatus(request.UserID(r), entryIDs, status); err != nil {
|
||||
if err := validator.ValidateEntriesStatusUpdateRequest(&entriesStatusUpdateRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.SetEntriesStatus(request.UserID(r), entriesStatusUpdateRequest.EntryIDs, entriesStatusUpdateRequest.Status); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
@@ -193,25 +174,30 @@ func (h *handler) toggleBookmark(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func configureFilters(builder *storage.EntryQueryBuilder, r *http.Request) {
|
||||
beforeEntryID := request.QueryInt64Param(r, "before_entry_id", 0)
|
||||
if beforeEntryID != 0 {
|
||||
if beforeEntryID > 0 {
|
||||
builder.BeforeEntryID(beforeEntryID)
|
||||
}
|
||||
|
||||
afterEntryID := request.QueryInt64Param(r, "after_entry_id", 0)
|
||||
if afterEntryID != 0 {
|
||||
if afterEntryID > 0 {
|
||||
builder.AfterEntryID(afterEntryID)
|
||||
}
|
||||
|
||||
beforeTimestamp := request.QueryInt64Param(r, "before", 0)
|
||||
if beforeTimestamp != 0 {
|
||||
if beforeTimestamp > 0 {
|
||||
builder.BeforeDate(time.Unix(beforeTimestamp, 0))
|
||||
}
|
||||
|
||||
afterTimestamp := request.QueryInt64Param(r, "after", 0)
|
||||
if afterTimestamp != 0 {
|
||||
if afterTimestamp > 0 {
|
||||
builder.AfterDate(time.Unix(afterTimestamp, 0))
|
||||
}
|
||||
|
||||
categoryID := request.QueryInt64Param(r, "category_id", 0)
|
||||
if categoryID > 0 {
|
||||
builder.WithCategoryID(categoryID)
|
||||
}
|
||||
|
||||
if request.HasQueryParam(r, "starred") {
|
||||
builder.WithStarred()
|
||||
}
|
||||
|
||||
+82
-44
@@ -5,61 +5,38 @@
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
feedHandler "miniflux.app/reader/handler"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func (h *handler) createFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedInfo, err := decodeFeedCreationPayload(r.Body)
|
||||
if err != nil {
|
||||
userID := request.UserID(r)
|
||||
|
||||
var feedCreationRequest model.FeedCreationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&feedCreationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if feedInfo.FeedURL == "" {
|
||||
json.BadRequest(w, r, errors.New("The feed_url is required"))
|
||||
if validationErr := validator.ValidateFeedCreation(h.store, userID, &feedCreationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if feedInfo.CategoryID <= 0 {
|
||||
json.BadRequest(w, r, errors.New("The category_id is required"))
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.UserID(r)
|
||||
|
||||
if h.store.FeedURLExists(userID, feedInfo.FeedURL) {
|
||||
json.BadRequest(w, r, errors.New("This feed_url already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
if !h.store.CategoryExists(userID, feedInfo.CategoryID) {
|
||||
json.BadRequest(w, r, errors.New("This category_id doesn't exists or doesn't belongs to this user"))
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := h.feedHandler.CreateFeed(
|
||||
userID,
|
||||
feedInfo.CategoryID,
|
||||
feedInfo.FeedURL,
|
||||
feedInfo.Crawler,
|
||||
feedInfo.UserAgent,
|
||||
feedInfo.Username,
|
||||
feedInfo.Password,
|
||||
)
|
||||
feed, err := feedHandler.CreateFeed(h.store, userID, &feedCreationRequest)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
type result struct {
|
||||
FeedID int64 `json:"feed_id"`
|
||||
}
|
||||
|
||||
json.Created(w, r, &result{FeedID: feed.ID})
|
||||
json.Created(w, r, &feedCreationResponse{FeedID: feed.ID})
|
||||
}
|
||||
|
||||
func (h *handler) refreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -71,7 +48,7 @@ func (h *handler) refreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err := h.feedHandler.RefreshFeed(userID, feedID)
|
||||
err := feedHandler.RefreshFeed(h.store, userID, feedID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -80,15 +57,30 @@ func (h *handler) refreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
feedChanges, err := decodeFeedModificationPayload(r.Body)
|
||||
func (h *handler) refreshAllFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
jobs, err := h.store.NewUserBatch(userID, h.store.CountFeeds(userID))
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
h.pool.Push(jobs)
|
||||
}()
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
var feedModificationRequest model.FeedModificationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&feedModificationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.UserID(r)
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
|
||||
originalFeed, err := h.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
@@ -101,13 +93,12 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
feedChanges.Update(originalFeed)
|
||||
|
||||
if !h.store.CategoryExists(userID, originalFeed.Category.ID) {
|
||||
json.BadRequest(w, r, errors.New("This category_id doesn't exists or doesn't belongs to this user"))
|
||||
if validationErr := validator.ValidateFeedModification(h.store, userID, &feedModificationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
feedModificationRequest.Patch(originalFeed)
|
||||
if err := h.store.UpdateFeed(originalFeed); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -122,6 +113,53 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
json.Created(w, r, originalFeed)
|
||||
}
|
||||
|
||||
func (h *handler) markFeedAsRead(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
userID := request.UserID(r)
|
||||
|
||||
feed, err := h.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if feed == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.MarkFeedAsRead(userID, feedID, time.Now()); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
func (h *handler) getCategoryFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
|
||||
category, err := h.store.Category(userID, categoryID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if category == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
feeds, err := h.store.FeedsByCategoryWithCounters(userID, categoryID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, feeds)
|
||||
}
|
||||
|
||||
func (h *handler) getFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
feeds, err := h.store.Feeds(request.UserID(r))
|
||||
if err != nil {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"miniflux.app/reader/feed"
|
||||
"miniflux.app/storage"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
store *storage.Storage
|
||||
feedHandler *feed.Handler
|
||||
}
|
||||
+1
-1
@@ -30,7 +30,7 @@ func (h *handler) feedIcon(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, &feedIcon{
|
||||
json.OK(w, r, &feedIconResponse{
|
||||
ID: icon.ID,
|
||||
MimeType: icon.MimeType,
|
||||
Data: icon.DataURL(),
|
||||
|
||||
+68
-8
@@ -21,40 +21,100 @@ type middleware struct {
|
||||
func newMiddleware(s *storage.Storage) *middleware {
|
||||
return &middleware{s}
|
||||
}
|
||||
|
||||
// BasicAuth handles HTTP basic authentication.
|
||||
func (m *middleware) serve(next http.Handler) http.Handler {
|
||||
func (m *middleware) handleCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP := request.ClientIP(r)
|
||||
token := r.Header.Get("X-Auth-Token")
|
||||
|
||||
if token == "" {
|
||||
logger.Debug("[API][TokenAuth] [ClientIP=%s] No API Key provided, go to the next middleware", clientIP)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.store.UserByAPIKey(token)
|
||||
if err != nil {
|
||||
logger.Error("[API][TokenAuth] %v", err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Error("[API][TokenAuth] [ClientIP=%s] No user found with the given API key", clientIP)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[API][TokenAuth] [ClientIP=%s] User authenticated: %s", clientIP, user.Username)
|
||||
m.store.SetLastLogin(user.ID)
|
||||
m.store.SetAPIKeyUsedTimestamp(user.ID, token)
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
|
||||
ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
|
||||
ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
|
||||
ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (m *middleware) basicAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if request.IsAuthenticated(r) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
|
||||
clientIP := request.ClientIP(r)
|
||||
username, password, authOK := r.BasicAuth()
|
||||
if !authOK {
|
||||
logger.Debug("[API] No authentication headers sent")
|
||||
logger.Debug("[API][BasicAuth] [ClientIP=%s] No authentication headers sent", clientIP)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if username == "" || password == "" {
|
||||
logger.Error("[API][BasicAuth] [ClientIP=%s] Empty username or password", clientIP)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.store.CheckPassword(username, password); err != nil {
|
||||
logger.Error("[API] [ClientIP=%s] Invalid username or password: %s", clientIP, username)
|
||||
logger.Error("[API][BasicAuth] [ClientIP=%s] Invalid username or password: %s", clientIP, username)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.store.UserByUsername(username)
|
||||
if err != nil {
|
||||
logger.Error("[API] %v", err)
|
||||
logger.Error("[API][BasicAuth] %v", err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Error("[API] [ClientIP=%s] User not found: %s", clientIP, username)
|
||||
logger.Error("[API][BasicAuth] [ClientIP=%s] User not found: %s", clientIP, username)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[API] User authenticated: %s", username)
|
||||
logger.Info("[API][BasicAuth] [ClientIP=%s] User authenticated: %s", clientIP, username)
|
||||
m.store.SetLastLogin(user.ID)
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
+3
-202
@@ -5,14 +5,10 @@
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"miniflux.app/model"
|
||||
)
|
||||
|
||||
type feedIcon struct {
|
||||
type feedIconResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Data string `json:"data"`
|
||||
@@ -23,201 +19,6 @@ type entriesResponse struct {
|
||||
Entries model.Entries `json:"entries"`
|
||||
}
|
||||
|
||||
type feedCreation struct {
|
||||
FeedURL string `json:"feed_url"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Crawler bool `json:"crawler"`
|
||||
}
|
||||
|
||||
type subscriptionDiscovery struct {
|
||||
URL string `json:"url"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type feedModification struct {
|
||||
FeedURL *string `json:"feed_url"`
|
||||
SiteURL *string `json:"site_url"`
|
||||
Title *string `json:"title"`
|
||||
ScraperRules *string `json:"scraper_rules"`
|
||||
RewriteRules *string `json:"rewrite_rules"`
|
||||
Crawler *bool `json:"crawler"`
|
||||
UserAgent *string `json:"user_agent"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
}
|
||||
|
||||
func (f *feedModification) Update(feed *model.Feed) {
|
||||
if f.FeedURL != nil && *f.FeedURL != "" {
|
||||
feed.FeedURL = *f.FeedURL
|
||||
}
|
||||
|
||||
if f.SiteURL != nil && *f.SiteURL != "" {
|
||||
feed.SiteURL = *f.SiteURL
|
||||
}
|
||||
|
||||
if f.Title != nil && *f.Title != "" {
|
||||
feed.Title = *f.Title
|
||||
}
|
||||
|
||||
if f.ScraperRules != nil {
|
||||
feed.ScraperRules = *f.ScraperRules
|
||||
}
|
||||
|
||||
if f.RewriteRules != nil {
|
||||
feed.RewriteRules = *f.RewriteRules
|
||||
}
|
||||
|
||||
if f.Crawler != nil {
|
||||
feed.Crawler = *f.Crawler
|
||||
}
|
||||
|
||||
if f.UserAgent != nil {
|
||||
feed.UserAgent = *f.UserAgent
|
||||
}
|
||||
|
||||
if f.Username != nil {
|
||||
feed.Username = *f.Username
|
||||
}
|
||||
|
||||
if f.Password != nil {
|
||||
feed.Password = *f.Password
|
||||
}
|
||||
|
||||
if f.CategoryID != nil && *f.CategoryID > 0 {
|
||||
feed.Category.ID = *f.CategoryID
|
||||
}
|
||||
}
|
||||
|
||||
type userModification struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
}
|
||||
|
||||
func (u *userModification) Update(user *model.User) {
|
||||
if u.Username != nil {
|
||||
user.Username = *u.Username
|
||||
}
|
||||
|
||||
if u.Password != nil {
|
||||
user.Password = *u.Password
|
||||
}
|
||||
|
||||
if u.IsAdmin != nil {
|
||||
user.IsAdmin = *u.IsAdmin
|
||||
}
|
||||
|
||||
if u.Theme != nil {
|
||||
user.Theme = *u.Theme
|
||||
}
|
||||
|
||||
if u.Language != nil {
|
||||
user.Language = *u.Language
|
||||
}
|
||||
|
||||
if u.Timezone != nil {
|
||||
user.Timezone = *u.Timezone
|
||||
}
|
||||
|
||||
if u.EntryDirection != nil {
|
||||
user.EntryDirection = *u.EntryDirection
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUserModificationPayload(r io.ReadCloser) (*userModification, error) {
|
||||
defer r.Close()
|
||||
|
||||
var user userModification
|
||||
decoder := json.NewDecoder(r)
|
||||
if err := decoder.Decode(&user); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode user modification JSON object: %v", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func decodeUserCreationPayload(r io.ReadCloser) (*model.User, error) {
|
||||
defer r.Close()
|
||||
|
||||
var user model.User
|
||||
decoder := json.NewDecoder(r)
|
||||
if err := decoder.Decode(&user); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode user modification JSON object: %v", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func decodeURLPayload(r io.ReadCloser) (*subscriptionDiscovery, error) {
|
||||
defer r.Close()
|
||||
|
||||
var s subscriptionDiscovery
|
||||
decoder := json.NewDecoder(r)
|
||||
if err := decoder.Decode(&s); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON payload: %v", err)
|
||||
}
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func decodeEntryStatusPayload(r io.ReadCloser) ([]int64, string, error) {
|
||||
type payload struct {
|
||||
EntryIDs []int64 `json:"entry_ids"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
var p payload
|
||||
decoder := json.NewDecoder(r)
|
||||
defer r.Close()
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
|
||||
}
|
||||
|
||||
return p.EntryIDs, p.Status, nil
|
||||
}
|
||||
|
||||
func decodeFeedCreationPayload(r io.ReadCloser) (*feedCreation, error) {
|
||||
defer r.Close()
|
||||
|
||||
var fc feedCreation
|
||||
decoder := json.NewDecoder(r)
|
||||
if err := decoder.Decode(&fc); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON payload: %v", err)
|
||||
}
|
||||
|
||||
return &fc, nil
|
||||
}
|
||||
|
||||
func decodeFeedModificationPayload(r io.ReadCloser) (*feedModification, error) {
|
||||
defer r.Close()
|
||||
|
||||
var feed feedModification
|
||||
decoder := json.NewDecoder(r)
|
||||
if err := decoder.Decode(&feed); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode feed modification JSON object: %v", err)
|
||||
}
|
||||
|
||||
return &feed, nil
|
||||
}
|
||||
|
||||
func decodeCategoryPayload(r io.ReadCloser) (*model.Category, error) {
|
||||
var category model.Category
|
||||
|
||||
decoder := json.NewDecoder(r)
|
||||
defer r.Close()
|
||||
if err := decoder.Decode(&category); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode category JSON object: %v", err)
|
||||
}
|
||||
|
||||
return &category, nil
|
||||
type feedCreationResponse struct {
|
||||
FeedID int64 `json:"feed_id"`
|
||||
}
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"miniflux.app/model"
|
||||
)
|
||||
|
||||
func TestUpdateFeedURL(t *testing.T) {
|
||||
feedURL := "http://example.com/"
|
||||
changes := &feedModification{FeedURL: &feedURL}
|
||||
feed := &model.Feed{FeedURL: "http://example.org/"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.FeedURL != feedURL {
|
||||
t.Fatalf(`Unexpected value, got %q instead of %q`, feed.FeedURL, feedURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedURLWithEmptyString(t *testing.T) {
|
||||
feedURL := ""
|
||||
changes := &feedModification{FeedURL: &feedURL}
|
||||
feed := &model.Feed{FeedURL: "http://example.org/"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.FeedURL == feedURL {
|
||||
t.Fatal(`The FeedURL should not be modified`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedURLWhenNotSet(t *testing.T) {
|
||||
changes := &feedModification{}
|
||||
feed := &model.Feed{FeedURL: "http://example.org/"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.FeedURL != "http://example.org/" {
|
||||
t.Fatal(`The FeedURL should not be modified`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedSiteURL(t *testing.T) {
|
||||
siteURL := "http://example.com/"
|
||||
changes := &feedModification{SiteURL: &siteURL}
|
||||
feed := &model.Feed{SiteURL: "http://example.org/"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.SiteURL != siteURL {
|
||||
t.Fatalf(`Unexpected value, got %q instead of %q`, feed.SiteURL, siteURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedSiteURLWithEmptyString(t *testing.T) {
|
||||
siteURL := ""
|
||||
changes := &feedModification{FeedURL: &siteURL}
|
||||
feed := &model.Feed{SiteURL: "http://example.org/"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.SiteURL == siteURL {
|
||||
t.Fatal(`The FeedURL should not be modified`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedSiteURLWhenNotSet(t *testing.T) {
|
||||
changes := &feedModification{}
|
||||
feed := &model.Feed{SiteURL: "http://example.org/"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.SiteURL != "http://example.org/" {
|
||||
t.Fatal(`The SiteURL should not be modified`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedTitle(t *testing.T) {
|
||||
title := "Example 2"
|
||||
changes := &feedModification{Title: &title}
|
||||
feed := &model.Feed{Title: "Example"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.Title != title {
|
||||
t.Fatalf(`Unexpected value, got %q instead of %q`, feed.Title, title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedTitleWithEmptyString(t *testing.T) {
|
||||
title := ""
|
||||
changes := &feedModification{Title: &title}
|
||||
feed := &model.Feed{Title: "Example"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.Title == title {
|
||||
t.Fatal(`The Title should not be modified`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedTitleWhenNotSet(t *testing.T) {
|
||||
changes := &feedModification{}
|
||||
feed := &model.Feed{Title: "Example"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.Title != "Example" {
|
||||
t.Fatal(`The Title should not be modified`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedUsername(t *testing.T) {
|
||||
username := "Alice"
|
||||
changes := &feedModification{Username: &username}
|
||||
feed := &model.Feed{Username: "Bob"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.Username != username {
|
||||
t.Fatalf(`Unexpected value, got %q instead of %q`, feed.Username, username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedUsernameWithEmptyString(t *testing.T) {
|
||||
username := ""
|
||||
changes := &feedModification{Username: &username}
|
||||
feed := &model.Feed{Username: "Bob"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.Username != "" {
|
||||
t.Fatal(`The Username should be empty now`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedUsernameWhenNotSet(t *testing.T) {
|
||||
changes := &feedModification{}
|
||||
feed := &model.Feed{Username: "Alice"}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.Username != "Alice" {
|
||||
t.Fatal(`The Username should not be modified`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedCategory(t *testing.T) {
|
||||
categoryID := int64(1)
|
||||
changes := &feedModification{CategoryID: &categoryID}
|
||||
feed := &model.Feed{Category: &model.Category{ID: 42}}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.Category.ID != categoryID {
|
||||
t.Fatalf(`Unexpected value, got %q instead of %q`, feed.Username, categoryID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedCategoryWithZero(t *testing.T) {
|
||||
categoryID := int64(0)
|
||||
changes := &feedModification{CategoryID: &categoryID}
|
||||
feed := &model.Feed{Category: &model.Category{ID: 42}}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.Category.ID != 42 {
|
||||
t.Fatal(`The CategoryID should not be modified`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFeedCategoryWhenNotSet(t *testing.T) {
|
||||
changes := &feedModification{}
|
||||
feed := &model.Feed{Category: &model.Category{ID: 42}}
|
||||
changes.Update(feed)
|
||||
|
||||
if feed.Category.ID != 42 {
|
||||
t.Fatal(`The CategoryID should not be modified`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserTheme(t *testing.T) {
|
||||
theme := "Example 2"
|
||||
changes := &userModification{Theme: &theme}
|
||||
user := &model.User{Theme: "Example"}
|
||||
changes.Update(user)
|
||||
|
||||
if user.Theme != theme {
|
||||
t.Fatalf(`Unexpected value, got %q instead of %q`, user.Theme, theme)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserThemeWhenNotSet(t *testing.T) {
|
||||
changes := &userModification{}
|
||||
user := &model.User{Theme: "Example"}
|
||||
changes.Update(user)
|
||||
|
||||
if user.Theme != "Example" {
|
||||
t.Fatal(`The user Theme should not be modified`)
|
||||
}
|
||||
}
|
||||
+19
-8
@@ -5,24 +5,35 @@
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/reader/subscription"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func (h *handler) getSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
subscriptionInfo, bodyErr := decodeURLPayload(r.Body)
|
||||
if bodyErr != nil {
|
||||
json.BadRequest(w, r, bodyErr)
|
||||
func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
var subscriptionDiscoveryRequest model.SubscriptionDiscoveryRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&subscriptionDiscoveryRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateSubscriptionDiscovery(&subscriptionDiscoveryRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
subscriptions, finderErr := subscription.FindSubscriptions(
|
||||
subscriptionInfo.URL,
|
||||
subscriptionInfo.UserAgent,
|
||||
subscriptionInfo.Username,
|
||||
subscriptionInfo.Password,
|
||||
subscriptionDiscoveryRequest.URL,
|
||||
subscriptionDiscoveryRequest.UserAgent,
|
||||
subscriptionDiscoveryRequest.Cookie,
|
||||
subscriptionDiscoveryRequest.Username,
|
||||
subscriptionDiscoveryRequest.Password,
|
||||
subscriptionDiscoveryRequest.FetchViaProxy,
|
||||
subscriptionDiscoveryRequest.AllowSelfSignedCertificates,
|
||||
)
|
||||
if finderErr != nil {
|
||||
json.ServerError(w, r, finderErr)
|
||||
|
||||
+50
-24
@@ -5,11 +5,14 @@
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
json_parser "encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func (h *handler) currentUser(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -28,48 +31,38 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := decodeUserCreationPayload(r.Body)
|
||||
if err != nil {
|
||||
var userCreationRequest model.UserCreationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&userCreationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.ValidateUserCreation(); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
if validationErr := validator.ValidateUserCreationWithPassword(h.store, &userCreationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if h.store.UserExists(user.Username) {
|
||||
json.BadRequest(w, r, errors.New("This user already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
err = h.store.CreateUser(user)
|
||||
user, err := h.store.CreateUser(&userCreationRequest)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
json.Created(w, r, user)
|
||||
}
|
||||
|
||||
func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
if !request.IsAdminUser(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.RouteInt64Param(r, "userID")
|
||||
userChanges, err := decodeUserModificationPayload(r.Body)
|
||||
if err != nil {
|
||||
|
||||
var userModificationRequest model.UserModificationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&userModificationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
originalUser, err := h.store.UserByID(userID)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,12 +71,24 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
userChanges.Update(originalUser)
|
||||
if err := originalUser.ValidateUserModification(); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
if !request.IsAdminUser(r) {
|
||||
if originalUser.ID != request.UserID(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if userModificationRequest.IsAdmin != nil && *userModificationRequest.IsAdmin {
|
||||
json.BadRequest(w, r, errors.New("Only administrators can change permissions of standard users"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateUserModification(h.store, originalUser.ID, &userModificationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
userModificationRequest.Patch(originalUser)
|
||||
if err = h.store.UpdateUser(originalUser); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -92,6 +97,26 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
json.Created(w, r, originalUser)
|
||||
}
|
||||
|
||||
func (h *handler) markUserAsRead(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.RouteInt64Param(r, "userID")
|
||||
if userID != request.UserID(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.store.UserByID(userID); err != nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.MarkAllAsRead(userID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
func (h *handler) users(w http.ResponseWriter, r *http.Request) {
|
||||
if !request.IsAdminUser(r) {
|
||||
json.Forbidden(w, r)
|
||||
@@ -169,10 +194,11 @@ func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.RemoveUser(user.ID); err != nil {
|
||||
json.BadRequest(w, r, errors.New("Unable to remove this user from the database"))
|
||||
if user.ID == request.UserID(r) {
|
||||
json.BadRequest(w, r, errors.New("You cannot remove yourself"))
|
||||
return
|
||||
}
|
||||
|
||||
h.store.RemoveUserAsync(user.ID)
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
+91
-14
@@ -10,8 +10,10 @@ import (
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/database"
|
||||
"miniflux.app/locale"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/ui/static"
|
||||
"miniflux.app/version"
|
||||
)
|
||||
|
||||
@@ -19,16 +21,20 @@ const (
|
||||
flagInfoHelp = "Show application information"
|
||||
flagVersionHelp = "Show application version"
|
||||
flagMigrateHelp = "Run SQL migrations"
|
||||
flagFlsuhSessionsHelp = "Flush all sessions (disconnect users)"
|
||||
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
|
||||
flagCreateAdminHelp = "Create admin user"
|
||||
flagResetPasswordHelp = "Reset user password"
|
||||
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
|
||||
flagDebugModeHelp = "Show debug logs"
|
||||
flagConfigFileHelp = "Load configuration file"
|
||||
flagConfigDumpHelp = "Print parsed configuration values"
|
||||
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
|
||||
)
|
||||
|
||||
// Parse parses command line arguments.
|
||||
func Parse() {
|
||||
var (
|
||||
err error
|
||||
flagInfo bool
|
||||
flagVersion bool
|
||||
flagMigrate bool
|
||||
@@ -37,6 +43,9 @@ func Parse() {
|
||||
flagResetPassword bool
|
||||
flagResetFeedErrors bool
|
||||
flagDebugMode bool
|
||||
flagConfigFile string
|
||||
flagConfigDump bool
|
||||
flagHealthCheck string
|
||||
)
|
||||
|
||||
flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)
|
||||
@@ -44,19 +53,49 @@ func Parse() {
|
||||
flag.BoolVar(&flagVersion, "version", false, flagVersionHelp)
|
||||
flag.BoolVar(&flagVersion, "v", false, flagVersionHelp)
|
||||
flag.BoolVar(&flagMigrate, "migrate", false, flagMigrateHelp)
|
||||
flag.BoolVar(&flagFlushSessions, "flush-sessions", false, flagFlsuhSessionsHelp)
|
||||
flag.BoolVar(&flagFlushSessions, "flush-sessions", false, flagFlushSessionsHelp)
|
||||
flag.BoolVar(&flagCreateAdmin, "create-admin", false, flagCreateAdminHelp)
|
||||
flag.BoolVar(&flagResetPassword, "reset-password", false, flagResetPasswordHelp)
|
||||
flag.BoolVar(&flagResetFeedErrors, "reset-feed-errors", false, flagResetFeedErrorsHelp)
|
||||
flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
|
||||
flag.StringVar(&flagConfigFile, "config-file", "", flagConfigFileHelp)
|
||||
flag.StringVar(&flagConfigFile, "c", "", flagConfigFileHelp)
|
||||
flag.BoolVar(&flagConfigDump, "config-dump", false, flagConfigDumpHelp)
|
||||
flag.StringVar(&flagHealthCheck, "healthcheck", "", flagHealthCheckHelp)
|
||||
flag.Parse()
|
||||
|
||||
cfg := config.NewConfig()
|
||||
cfg := config.NewParser()
|
||||
|
||||
if flagDebugMode || cfg.HasDebugMode() {
|
||||
if flagConfigFile != "" {
|
||||
config.Opts, err = cfg.ParseFile(flagConfigFile)
|
||||
if err != nil {
|
||||
logger.Fatal("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
config.Opts, err = cfg.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
logger.Fatal("%v", err)
|
||||
}
|
||||
|
||||
if flagConfigDump {
|
||||
fmt.Print(config.Opts)
|
||||
return
|
||||
}
|
||||
|
||||
if config.Opts.LogDateTime() {
|
||||
logger.EnableDateTime()
|
||||
}
|
||||
|
||||
if flagDebugMode || config.Opts.HasDebugMode() {
|
||||
logger.EnableDebug()
|
||||
}
|
||||
|
||||
if flagHealthCheck != "" {
|
||||
doHealthCheck(flagHealthCheck)
|
||||
return
|
||||
}
|
||||
|
||||
if flagInfo {
|
||||
info()
|
||||
return
|
||||
@@ -67,18 +106,50 @@ func Parse() {
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.NewConnectionPool(cfg.DatabaseURL(), cfg.DatabaseMinConns(), cfg.DatabaseMaxConns())
|
||||
if config.Opts.IsDefaultDatabaseURL() {
|
||||
logger.Info("The default value for DATABASE_URL is used")
|
||||
}
|
||||
|
||||
logger.Debug("Loading translations...")
|
||||
if err := locale.LoadCatalogMessages(); err != nil {
|
||||
logger.Fatal("Unable to load translations: %v", err)
|
||||
}
|
||||
|
||||
logger.Debug("Loading static assets...")
|
||||
if err := static.CalculateBinaryFileChecksums(); err != nil {
|
||||
logger.Fatal("Unable to calculate binary files checksum: %v", err)
|
||||
}
|
||||
|
||||
if err := static.GenerateStylesheetsBundles(); err != nil {
|
||||
logger.Fatal("Unable to generate Stylesheet bundles: %v", err)
|
||||
}
|
||||
|
||||
if err := static.GenerateJavascriptBundles(); err != nil {
|
||||
logger.Fatal("Unable to generate Javascript bundles: %v", err)
|
||||
}
|
||||
|
||||
db, err := database.NewConnectionPool(
|
||||
config.Opts.DatabaseURL(),
|
||||
config.Opts.DatabaseMinConns(),
|
||||
config.Opts.DatabaseMaxConns(),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal("Unable to connect to the database: %v", err)
|
||||
logger.Fatal("Unable to initialize database connection pool: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if flagMigrate {
|
||||
database.Migrate(db)
|
||||
return
|
||||
store := storage.NewStorage(db)
|
||||
|
||||
if err := store.Ping(); err != nil {
|
||||
logger.Fatal("Unable to connect to the database: %v", err)
|
||||
}
|
||||
|
||||
store := storage.NewStorage(db)
|
||||
if flagMigrate {
|
||||
if err := database.Migrate(db); err != nil {
|
||||
logger.Fatal(`%v`, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if flagResetFeedErrors {
|
||||
store.ResetFeedErrors()
|
||||
@@ -101,14 +172,20 @@ func Parse() {
|
||||
}
|
||||
|
||||
// Run migrations and start the deamon.
|
||||
if cfg.RunMigrations() {
|
||||
database.Migrate(db)
|
||||
if config.Opts.RunMigrations() {
|
||||
if err := database.Migrate(db); err != nil {
|
||||
logger.Fatal(`%v`, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.IsSchemaUpToDate(db); err != nil {
|
||||
logger.Fatal(`You must run the SQL migrations, %v`, err)
|
||||
}
|
||||
|
||||
// Create admin user and start the deamon.
|
||||
if cfg.CreateAdmin() {
|
||||
if config.Opts.CreateAdmin() {
|
||||
createAdmin(store)
|
||||
}
|
||||
|
||||
startDaemon(cfg, store)
|
||||
startDaemon(store)
|
||||
}
|
||||
|
||||
+16
-13
@@ -8,32 +8,35 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func createAdmin(store *storage.Storage) {
|
||||
user := model.NewUser()
|
||||
user.Username = os.Getenv("ADMIN_USERNAME")
|
||||
user.Password = os.Getenv("ADMIN_PASSWORD")
|
||||
user.IsAdmin = true
|
||||
|
||||
if user.Username == "" || user.Password == "" {
|
||||
user.Username, user.Password = askCredentials()
|
||||
userCreationRequest := &model.UserCreationRequest{
|
||||
Username: config.Opts.AdminUsername(),
|
||||
Password: config.Opts.AdminPassword(),
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
if err := user.ValidateUserCreation(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
if userCreationRequest.Username == "" || userCreationRequest.Password == "" {
|
||||
userCreationRequest.Username, userCreationRequest.Password = askCredentials()
|
||||
}
|
||||
|
||||
if store.UserExists(user.Username) {
|
||||
logger.Info(`User %q already exists, skipping creation`, user.Username)
|
||||
if store.UserExists(userCreationRequest.Username) {
|
||||
logger.Info(`User %q already exists, skipping creation`, userCreationRequest.Username)
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.CreateUser(user); err != nil {
|
||||
if validationErr := validator.ValidateUserCreationWithPassword(store, userCreationRequest); validationErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := store.CreateUser(userCreationRequest); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
+18
-23
@@ -9,38 +9,44 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/reader/feed"
|
||||
"miniflux.app/service/scheduler"
|
||||
"miniflux.app/metric"
|
||||
"miniflux.app/service/httpd"
|
||||
"miniflux.app/service/scheduler"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/worker"
|
||||
)
|
||||
|
||||
func startDaemon(cfg *config.Config, store *storage.Storage) {
|
||||
func startDaemon(store *storage.Storage) {
|
||||
logger.Info("Starting Miniflux...")
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt)
|
||||
signal.Notify(stop, syscall.SIGTERM)
|
||||
|
||||
feedHandler := feed.NewFeedHandler(store)
|
||||
pool := worker.NewPool(feedHandler, cfg.WorkerPoolSize())
|
||||
pool := worker.NewPool(store, config.Opts.WorkerPoolSize())
|
||||
|
||||
go showProcessStatistics()
|
||||
|
||||
if cfg.HasSchedulerService() {
|
||||
scheduler.Serve(cfg, store, pool)
|
||||
if config.Opts.HasSchedulerService() && !config.Opts.HasMaintenanceMode() {
|
||||
scheduler.Serve(store, pool)
|
||||
}
|
||||
|
||||
var httpServer *http.Server
|
||||
if cfg.HasHTTPService() {
|
||||
httpServer = httpd.Serve(cfg, store, pool, feedHandler)
|
||||
if config.Opts.HasHTTPService() {
|
||||
httpServer = httpd.Serve(store, pool)
|
||||
}
|
||||
|
||||
if config.Opts.HasMetricsCollector() {
|
||||
collector := metric.NewCollector(store, config.Opts.MetricsRefreshInterval())
|
||||
go collector.GatherStorageMetrics()
|
||||
}
|
||||
|
||||
// Notify systemd that we are ready.
|
||||
if err := sdNotify(sdNotifyReady); err != nil {
|
||||
logger.Error("Unable to send readiness notification to systemd: %v", err)
|
||||
}
|
||||
|
||||
<-stop
|
||||
@@ -54,14 +60,3 @@ func startDaemon(cfg *config.Config, store *storage.Storage) {
|
||||
|
||||
logger.Info("Process gracefully stopped")
|
||||
}
|
||||
|
||||
func showProcessStatistics() {
|
||||
for {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
logger.Debug("Sys=%vK, InUse=%vK, HeapInUse=%vK, StackSys=%vK, StackInUse=%vK, GoRoutines=%d, NumCPU=%d",
|
||||
m.Sys/1024, (m.Sys-m.HeapReleased)/1024, m.HeapInuse/1024, m.StackSys/1024, m.StackInuse/1024,
|
||||
runtime.NumGoroutine(), runtime.NumCPU())
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2021 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/logger"
|
||||
)
|
||||
|
||||
func doHealthCheck(healthCheckEndpoint string) {
|
||||
if healthCheckEndpoint == "auto" {
|
||||
healthCheckEndpoint = "http://" + config.Opts.ListenAddr() + config.Opts.BasePath() + "/healthcheck"
|
||||
}
|
||||
|
||||
logger.Debug(`Executing health check on %s`, healthCheckEndpoint)
|
||||
|
||||
client := &http.Client{Timeout: 3 * time.Second}
|
||||
resp, err := client.Get(healthCheckEndpoint)
|
||||
if err != nil {
|
||||
logger.Fatal(`Health check failure: %v`, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Fatal(`Health check failed with status code %d`, resp.StatusCode)
|
||||
}
|
||||
|
||||
logger.Debug(`Health check is OK`)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
func info() {
|
||||
fmt.Println("Version:", version.Version)
|
||||
fmt.Println("Commit:", version.Commit)
|
||||
fmt.Println("Build Date:", version.BuildDate)
|
||||
fmt.Println("Go Version:", runtime.Version())
|
||||
fmt.Println("Compiler:", runtime.Compiler)
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func resetPassword(store *storage.Storage) {
|
||||
@@ -24,12 +26,15 @@ func resetPassword(store *storage.Storage) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
user.Password = password
|
||||
if err := user.ValidatePassword(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
userModificationRequest := &model.UserModificationRequest{
|
||||
Password: &password,
|
||||
}
|
||||
if validationErr := validator.ValidateUserModification(store, user.ID, userModificationRequest); validationErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
user.Password = password
|
||||
if err := store.UpdateUser(user); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2021 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
// sdNotifyReady tells the service manager that service startup is
|
||||
// finished, or the service finished loading its configuration.
|
||||
sdNotifyReady = "READY=1"
|
||||
)
|
||||
|
||||
// sdNotify sends a message to systemd using the sd_notify protocol.
|
||||
// See https://www.freedesktop.org/software/systemd/man/sd_notify.html.
|
||||
func sdNotify(state string) error {
|
||||
addr := &net.UnixAddr{
|
||||
Net: "unixgram",
|
||||
Name: os.Getenv("NOTIFY_SOCKET"),
|
||||
}
|
||||
|
||||
if addr.Name == "" {
|
||||
// We're not running under systemd (NOTIFY_SOCKET has not set).
|
||||
return nil
|
||||
}
|
||||
|
||||
conn, err := net.DialUnix(addr.Net, nil, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if _, err = conn.Write([]byte(state)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,8 @@
|
||||
Miniflux API Client
|
||||
===================
|
||||
|
||||
[](https://pkg.go.dev/miniflux.app/client)
|
||||
|
||||
Client library for Miniflux REST API.
|
||||
|
||||
Installation
|
||||
@@ -18,14 +20,18 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
miniflux "miniflux.app/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Authentication with username/password:
|
||||
client := miniflux.New("https://api.example.org", "admin", "secret")
|
||||
|
||||
// Authentication with an API Key:
|
||||
client := miniflux.New("https://api.example.org", "my-secret-token")
|
||||
|
||||
// Fetch all feeds.
|
||||
feeds, err := client.Feeds()
|
||||
if err != nil {
|
||||
@@ -41,7 +47,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile("opml.xml", opml, 0644)
|
||||
err = os.WriteFile("opml.xml", opml, 0644)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
|
||||
+116
-55
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
@@ -18,6 +17,14 @@ type Client struct {
|
||||
request *request
|
||||
}
|
||||
|
||||
// New returns a new Miniflux client.
|
||||
func New(endpoint string, credentials ...string) *Client {
|
||||
if len(credentials) == 2 {
|
||||
return &Client{request: &request{endpoint: endpoint, username: credentials[0], password: credentials[1]}}
|
||||
}
|
||||
return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}}
|
||||
}
|
||||
|
||||
// Me returns the logged user information.
|
||||
func (c *Client) Me() (*User, error) {
|
||||
body, err := c.request.Get("/v1/me")
|
||||
@@ -88,7 +95,11 @@ func (c *Client) UserByUsername(username string) (*User, error) {
|
||||
|
||||
// CreateUser creates a new user in the system.
|
||||
func (c *Client) CreateUser(username, password string, isAdmin bool) (*User, error) {
|
||||
body, err := c.request.Post("/v1/users", &User{Username: username, Password: password, IsAdmin: isAdmin})
|
||||
body, err := c.request.Post("/v1/users", &UserCreationRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
IsAdmin: isAdmin,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -104,7 +115,7 @@ func (c *Client) CreateUser(username, password string, isAdmin bool) (*User, err
|
||||
}
|
||||
|
||||
// UpdateUser updates a user in the system.
|
||||
func (c *Client) UpdateUser(userID int64, userChanges *UserModification) (*User, error) {
|
||||
func (c *Client) UpdateUser(userID int64, userChanges *UserModificationRequest) (*User, error) {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/users/%d", userID), userChanges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -122,12 +133,13 @@ func (c *Client) UpdateUser(userID int64, userChanges *UserModification) (*User,
|
||||
|
||||
// DeleteUser removes a user from the system.
|
||||
func (c *Client) DeleteUser(userID int64) error {
|
||||
body, err := c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
return nil
|
||||
return c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
|
||||
}
|
||||
|
||||
// MarkAllAsRead marks all unread entries as read for a given user.
|
||||
func (c *Client) MarkAllAsRead(userID int64) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/users/%d/mark-all-as-read", userID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Discover try to find subscriptions from a website.
|
||||
@@ -204,15 +216,32 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// DeleteCategory removes a category.
|
||||
func (c *Client) DeleteCategory(categoryID int64) error {
|
||||
body, err := c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
|
||||
// MarkCategoryAsRead marks all unread entries in a category as read.
|
||||
func (c *Client) MarkCategoryAsRead(categoryID int64) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// CategoryFeeds gets feeds of a cateogry.
|
||||
func (c *Client) CategoryFeeds(categoryID int64) (Feeds, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/feeds", categoryID))
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
return nil
|
||||
var feeds Feeds
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&feeds); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return feeds, nil
|
||||
}
|
||||
|
||||
// DeleteCategory removes a category.
|
||||
func (c *Client) DeleteCategory(categoryID int64) error {
|
||||
return c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
|
||||
}
|
||||
|
||||
// Feeds gets all feeds.
|
||||
@@ -240,7 +269,7 @@ func (c *Client) Export() ([]byte, error) {
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
opml, err := ioutil.ReadAll(body)
|
||||
opml, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -272,11 +301,8 @@ func (c *Client) Feed(feedID int64) (*Feed, error) {
|
||||
}
|
||||
|
||||
// CreateFeed creates a new feed.
|
||||
func (c *Client) CreateFeed(url string, categoryID int64) (int64, error) {
|
||||
body, err := c.request.Post("/v1/feeds", map[string]interface{}{
|
||||
"feed_url": url,
|
||||
"category_id": categoryID,
|
||||
})
|
||||
func (c *Client) CreateFeed(feedCreationRequest *FeedCreationRequest) (int64, error) {
|
||||
body, err := c.request.Post("/v1/feeds", feedCreationRequest)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -296,7 +322,7 @@ func (c *Client) CreateFeed(url string, categoryID int64) (int64, error) {
|
||||
}
|
||||
|
||||
// UpdateFeed updates a feed.
|
||||
func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModification) (*Feed, error) {
|
||||
func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModificationRequest) (*Feed, error) {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d", feedID), feedChanges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -304,32 +330,34 @@ func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModification) (*Feed,
|
||||
defer body.Close()
|
||||
|
||||
var f *Feed
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&f); err != nil {
|
||||
if err := json.NewDecoder(body).Decode(&f); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// RefreshFeed refresh a feed.
|
||||
// MarkFeedAsRead marks all unread entries of the feed as read.
|
||||
func (c *Client) MarkFeedAsRead(feedID int64) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/mark-all-as-read", feedID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// RefreshAllFeeds refreshes all feeds.
|
||||
func (c *Client) RefreshAllFeeds() error {
|
||||
_, err := c.request.Put("/v1/feeds/refresh", nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// RefreshFeed refreshes a feed.
|
||||
func (c *Client) RefreshFeed(feedID int64) error {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/refresh", feedID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
return nil
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/refresh", feedID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteFeed removes a feed.
|
||||
func (c *Client) DeleteFeed(feedID int64) error {
|
||||
body, err := c.request.Delete(fmt.Sprintf("/v1/feeds/%d", feedID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
return nil
|
||||
return c.request.Delete(fmt.Sprintf("/v1/feeds/%d", feedID))
|
||||
}
|
||||
|
||||
// FeedIcon gets a feed icon.
|
||||
@@ -366,6 +394,23 @@ func (c *Client) FeedEntry(feedID, entryID int64) (*Entry, error) {
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// CategoryEntry gets a single category entry.
|
||||
func (c *Client) CategoryEntry(categoryID, entryID int64) (*Entry, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/entries/%d", categoryID, entryID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var entry *Entry
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&entry); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// Entry gets a single entry.
|
||||
func (c *Client) Entry(entryID int64) (*Entry, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d", entryID))
|
||||
@@ -421,6 +466,25 @@ func (c *Client) FeedEntries(feedID int64, filter *Filter) (*EntryResultSet, err
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CategoryEntries fetch entries of a category.
|
||||
func (c *Client) CategoryEntries(categoryID int64, filter *Filter) (*EntryResultSet, error) {
|
||||
path := buildFilterQueryString(fmt.Sprintf("/v1/categories/%d/entries", categoryID), filter)
|
||||
|
||||
body, err := c.request.Get(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var result EntryResultSet
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// UpdateEntries updates the status of a list of entries.
|
||||
func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
|
||||
type payload struct {
|
||||
@@ -428,29 +492,14 @@ func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
body, err := c.request.Put("/v1/entries", &payload{EntryIDs: entryIDs, Status: status})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
|
||||
return nil
|
||||
_, err := c.request.Put("/v1/entries", &payload{EntryIDs: entryIDs, Status: status})
|
||||
return err
|
||||
}
|
||||
|
||||
// ToggleBookmark toggles entry bookmark value.
|
||||
func (c *Client) ToggleBookmark(entryID int64) error {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// New returns a new Miniflux client.
|
||||
func New(endpoint, username, password string) *Client {
|
||||
return &Client{request: &request{endpoint: endpoint, username: username, password: password}}
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func buildFilterQueryString(path string, filter *Filter) string {
|
||||
@@ -501,6 +550,18 @@ func buildFilterQueryString(path string, filter *Filter) string {
|
||||
values.Set("search", filter.Search)
|
||||
}
|
||||
|
||||
if filter.CategoryID > 0 {
|
||||
values.Set("category_id", strconv.FormatInt(filter.CategoryID, 10))
|
||||
}
|
||||
|
||||
if filter.FeedID > 0 {
|
||||
values.Set("feed_id", strconv.FormatInt(filter.FeedID, 10))
|
||||
}
|
||||
|
||||
for _, status := range filter.Statuses {
|
||||
values.Add("status", status)
|
||||
}
|
||||
|
||||
path = fmt.Sprintf("%s?%s", path, values.Encode())
|
||||
}
|
||||
|
||||
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package client // import "miniflux.app/client"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Entry statuses.
|
||||
const (
|
||||
EntryStatusUnread = "unread"
|
||||
EntryStatusRead = "read"
|
||||
EntryStatusRemoved = "removed"
|
||||
)
|
||||
|
||||
// User represents a user in the system.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Theme string `json:"theme"`
|
||||
Language string `json:"language"`
|
||||
Timezone string `json:"timezone"`
|
||||
EntryDirection string `json:"entry_sorting_direction"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
Extra map[string]string `json:"extra"`
|
||||
}
|
||||
|
||||
func (u User) String() string {
|
||||
return fmt.Sprintf("#%d - %s (admin=%v)", u.ID, u.Username, u.IsAdmin)
|
||||
}
|
||||
|
||||
// UserModification is used to update a user.
|
||||
type UserModification struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
}
|
||||
|
||||
// Users represents a list of users.
|
||||
type Users []User
|
||||
|
||||
// Category represents a category in the system.
|
||||
type Category struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
func (c Category) String() string {
|
||||
return fmt.Sprintf("#%d %s", c.ID, c.Title)
|
||||
}
|
||||
|
||||
// Categories represents a list of categories.
|
||||
type Categories []*Category
|
||||
|
||||
// Subscription represents a feed subscription.
|
||||
type Subscription struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (s Subscription) String() string {
|
||||
return fmt.Sprintf(`Title="%s", URL="%s", Type="%s"`, s.Title, s.URL, s.Type)
|
||||
}
|
||||
|
||||
// Subscriptions represents a list of subscriptions.
|
||||
type Subscriptions []*Subscription
|
||||
|
||||
// Feed represents a Miniflux feed.
|
||||
type Feed struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedURL string `json:"feed_url"`
|
||||
SiteURL string `json:"site_url"`
|
||||
Title string `json:"title"`
|
||||
CheckedAt time.Time `json:"checked_at,omitempty"`
|
||||
EtagHeader string `json:"etag_header,omitempty"`
|
||||
LastModifiedHeader string `json:"last_modified_header,omitempty"`
|
||||
ParsingErrorMsg string `json:"parsing_error_message,omitempty"`
|
||||
ParsingErrorCount int `json:"parsing_error_count,omitempty"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
Crawler bool `json:"crawler"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Category *Category `json:"category,omitempty"`
|
||||
Entries Entries `json:"entries,omitempty"`
|
||||
}
|
||||
|
||||
// FeedModification represents changes for a feed.
|
||||
type FeedModification struct {
|
||||
FeedURL *string `json:"feed_url"`
|
||||
SiteURL *string `json:"site_url"`
|
||||
Title *string `json:"title"`
|
||||
ScraperRules *string `json:"scraper_rules"`
|
||||
RewriteRules *string `json:"rewrite_rules"`
|
||||
Crawler *bool `json:"crawler"`
|
||||
UserAgent *string `json:"user_agent"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
}
|
||||
|
||||
// FeedIcon represents the feed icon.
|
||||
type FeedIcon struct {
|
||||
ID int64 `json:"id"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Feeds represents a list of feeds.
|
||||
type Feeds []*Feed
|
||||
|
||||
// Entry represents a subscription item in the system.
|
||||
type Entry struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Status string `json:"status"`
|
||||
Hash string `json:"hash"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Date time.Time `json:"published_at"`
|
||||
Content string `json:"content"`
|
||||
Author string `json:"author"`
|
||||
Starred bool `json:"starred"`
|
||||
Enclosures Enclosures `json:"enclosures,omitempty"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
Category *Category `json:"category,omitempty"`
|
||||
}
|
||||
|
||||
// Entries represents a list of entries.
|
||||
type Entries []*Entry
|
||||
|
||||
// Enclosure represents an attachment.
|
||||
type Enclosure struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
EntryID int64 `json:"entry_id"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// Enclosures represents a list of attachments.
|
||||
type Enclosures []*Enclosure
|
||||
|
||||
// Filter is used to filter entries.
|
||||
type Filter struct {
|
||||
Status string
|
||||
Offset int
|
||||
Limit int
|
||||
Order string
|
||||
Direction string
|
||||
Starred bool
|
||||
Before int64
|
||||
After int64
|
||||
BeforeEntryID int64
|
||||
AfterEntryID int64
|
||||
Search string
|
||||
}
|
||||
|
||||
// EntryResultSet represents the response when fetching entries.
|
||||
type EntryResultSet struct {
|
||||
Total int `json:"total"`
|
||||
Entries Entries `json:"entries"`
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package client // import "miniflux.app/client"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Entry statuses.
|
||||
const (
|
||||
EntryStatusUnread = "unread"
|
||||
EntryStatusRead = "read"
|
||||
EntryStatusRemoved = "removed"
|
||||
)
|
||||
|
||||
// User represents a user in the system.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Theme string `json:"theme"`
|
||||
Language string `json:"language"`
|
||||
Timezone string `json:"timezone"`
|
||||
EntryDirection string `json:"entry_sorting_direction"`
|
||||
Stylesheet string `json:"stylesheet"`
|
||||
GoogleID string `json:"google_id"`
|
||||
OpenIDConnectID string `json:"openid_connect_id"`
|
||||
EntriesPerPage int `json:"entries_per_page"`
|
||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime bool `json:"show_reading_time"`
|
||||
EntrySwipe bool `json:"entry_swipe"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
DisplayMode string `json:"display_mode"`
|
||||
}
|
||||
|
||||
func (u User) String() string {
|
||||
return fmt.Sprintf("#%d - %s (admin=%v)", u.ID, u.Username, u.IsAdmin)
|
||||
}
|
||||
|
||||
// UserCreationRequest represents the request to create a user.
|
||||
type UserCreationRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
GoogleID string `json:"google_id"`
|
||||
OpenIDConnectID string `json:"openid_connect_id"`
|
||||
}
|
||||
|
||||
// UserModificationRequest represents the request to update a user.
|
||||
type UserModificationRequest struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
Stylesheet *string `json:"stylesheet"`
|
||||
GoogleID *string `json:"google_id"`
|
||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||
EntriesPerPage *int `json:"entries_per_page"`
|
||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime *bool `json:"show_reading_time"`
|
||||
EntrySwipe *bool `json:"entry_swipe"`
|
||||
DisplayMode *string `json:"display_mode"`
|
||||
}
|
||||
|
||||
// Users represents a list of users.
|
||||
type Users []User
|
||||
|
||||
// Category represents a feed category.
|
||||
type Category struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
func (c Category) String() string {
|
||||
return fmt.Sprintf("#%d %s", c.ID, c.Title)
|
||||
}
|
||||
|
||||
// Categories represents a list of categories.
|
||||
type Categories []*Category
|
||||
|
||||
// Subscription represents a feed subscription.
|
||||
type Subscription struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (s Subscription) String() string {
|
||||
return fmt.Sprintf(`Title="%s", URL="%s", Type="%s"`, s.Title, s.URL, s.Type)
|
||||
}
|
||||
|
||||
// Subscriptions represents a list of subscriptions.
|
||||
type Subscriptions []*Subscription
|
||||
|
||||
// Feed represents a Miniflux feed.
|
||||
type Feed struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedURL string `json:"feed_url"`
|
||||
SiteURL string `json:"site_url"`
|
||||
Title string `json:"title"`
|
||||
CheckedAt time.Time `json:"checked_at,omitempty"`
|
||||
EtagHeader string `json:"etag_header,omitempty"`
|
||||
LastModifiedHeader string `json:"last_modified_header,omitempty"`
|
||||
ParsingErrorMsg string `json:"parsing_error_message,omitempty"`
|
||||
ParsingErrorCount int `json:"parsing_error_count,omitempty"`
|
||||
Disabled bool `json:"disabled"`
|
||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
Crawler bool `json:"crawler"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Cookie string `json:"cookie"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Category *Category `json:"category,omitempty"`
|
||||
}
|
||||
|
||||
// FeedCreationRequest represents the request to create a feed.
|
||||
type FeedCreationRequest struct {
|
||||
FeedURL string `json:"feed_url"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Cookie string `json:"cookie"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Crawler bool `json:"crawler"`
|
||||
Disabled bool `json:"disabled"`
|
||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
}
|
||||
|
||||
// FeedModificationRequest represents the request to update a feed.
|
||||
type FeedModificationRequest struct {
|
||||
FeedURL *string `json:"feed_url"`
|
||||
SiteURL *string `json:"site_url"`
|
||||
Title *string `json:"title"`
|
||||
ScraperRules *string `json:"scraper_rules"`
|
||||
RewriteRules *string `json:"rewrite_rules"`
|
||||
BlocklistRules *string `json:"blocklist_rules"`
|
||||
KeeplistRules *string `json:"keeplist_rules"`
|
||||
Crawler *bool `json:"crawler"`
|
||||
UserAgent *string `json:"user_agent"`
|
||||
Cookie *string `json:"cookie"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
Disabled *bool `json:"disabled"`
|
||||
IgnoreHTTPCache *bool `json:"ignore_http_cache"`
|
||||
AllowSelfSignedCertificates *bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy *bool `json:"fetch_via_proxy"`
|
||||
}
|
||||
|
||||
// FeedIcon represents the feed icon.
|
||||
type FeedIcon struct {
|
||||
ID int64 `json:"id"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Feeds represents a list of feeds.
|
||||
type Feeds []*Feed
|
||||
|
||||
// Entry represents a subscription item in the system.
|
||||
type Entry struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Status string `json:"status"`
|
||||
Hash string `json:"hash"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Date time.Time `json:"published_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Content string `json:"content"`
|
||||
Author string `json:"author"`
|
||||
ShareCode string `json:"share_code"`
|
||||
Starred bool `json:"starred"`
|
||||
ReadingTime int `json:"reading_time"`
|
||||
Enclosures Enclosures `json:"enclosures,omitempty"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
}
|
||||
|
||||
// Entries represents a list of entries.
|
||||
type Entries []*Entry
|
||||
|
||||
// Enclosure represents an attachment.
|
||||
type Enclosure struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
EntryID int64 `json:"entry_id"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// Enclosures represents a list of attachments.
|
||||
type Enclosures []*Enclosure
|
||||
|
||||
// Filter is used to filter entries.
|
||||
type Filter struct {
|
||||
Status string
|
||||
Offset int
|
||||
Limit int
|
||||
Order string
|
||||
Direction string
|
||||
Starred bool
|
||||
Before int64
|
||||
After int64
|
||||
BeforeEntryID int64
|
||||
AfterEntryID int64
|
||||
Search string
|
||||
CategoryID int64
|
||||
FeedID int64
|
||||
Statuses []string
|
||||
}
|
||||
|
||||
// EntryResultSet represents the response when fetching entries.
|
||||
type EntryResultSet struct {
|
||||
Total int `json:"total"`
|
||||
Entries Entries `json:"entries"`
|
||||
}
|
||||
+22
-7
@@ -10,7 +10,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -38,6 +37,7 @@ type request struct {
|
||||
endpoint string
|
||||
username string
|
||||
password string
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func (r *request) Get(path string) (io.ReadCloser, error) {
|
||||
@@ -56,8 +56,9 @@ func (r *request) Put(path string, data interface{}) (io.ReadCloser, error) {
|
||||
return r.execute(http.MethodPut, path, data)
|
||||
}
|
||||
|
||||
func (r *request) Delete(path string) (io.ReadCloser, error) {
|
||||
return r.execute(http.MethodDelete, path, nil)
|
||||
func (r *request) Delete(path string) error {
|
||||
_, err := r.execute(http.MethodDelete, path, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, error) {
|
||||
@@ -75,14 +76,17 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
|
||||
Method: method,
|
||||
Header: r.buildHeaders(),
|
||||
}
|
||||
request.SetBasicAuth(r.username, r.password)
|
||||
|
||||
if r.username != "" && r.password != "" {
|
||||
request.SetBasicAuth(r.username, r.password)
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
switch data.(type) {
|
||||
switch data := data.(type) {
|
||||
case io.ReadCloser:
|
||||
request.Body = data.(io.ReadCloser)
|
||||
request.Body = data
|
||||
default:
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(r.toJSON(data)))
|
||||
request.Body = io.NopCloser(bytes.NewBuffer(r.toJSON(data)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,13 +98,20 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusUnauthorized:
|
||||
response.Body.Close()
|
||||
return nil, ErrNotAuthorized
|
||||
case http.StatusForbidden:
|
||||
response.Body.Close()
|
||||
return nil, ErrForbidden
|
||||
case http.StatusInternalServerError:
|
||||
response.Body.Close()
|
||||
return nil, ErrServerError
|
||||
case http.StatusNotFound:
|
||||
response.Body.Close()
|
||||
return nil, ErrNotFound
|
||||
case http.StatusNoContent:
|
||||
response.Body.Close()
|
||||
return nil, nil
|
||||
case http.StatusBadRequest:
|
||||
defer response.Body.Close()
|
||||
|
||||
@@ -114,6 +125,7 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
|
||||
}
|
||||
|
||||
if response.StatusCode > 400 {
|
||||
response.Body.Close()
|
||||
return nil, fmt.Errorf("miniflux: status code=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
@@ -131,6 +143,9 @@ func (r *request) buildHeaders() http.Header {
|
||||
headers.Add("User-Agent", userAgent)
|
||||
headers.Add("Content-Type", "application/json")
|
||||
headers.Add("Accept", "application/json")
|
||||
if r.apiKey != "" {
|
||||
headers.Add("X-Auth-Token", r.apiKey)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
|
||||
+2
-268
@@ -4,271 +4,5 @@
|
||||
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBaseURL = "http://localhost"
|
||||
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
|
||||
defaultWorkerPoolSize = 5
|
||||
defaultPollingFrequency = 60
|
||||
defaultBatchSize = 10
|
||||
defaultDatabaseMaxConns = 20
|
||||
defaultDatabaseMinConns = 1
|
||||
defaultArchiveReadDays = 60
|
||||
defaultListenAddr = "127.0.0.1:8080"
|
||||
defaultCertFile = ""
|
||||
defaultKeyFile = ""
|
||||
defaultCertDomain = ""
|
||||
defaultCertCache = "/tmp/cert_cache"
|
||||
defaultCleanupFrequency = 24
|
||||
defaultProxyImages = "http-only"
|
||||
defaultOAuth2ClientID = ""
|
||||
defaultOAuth2ClientSecret = ""
|
||||
defaultOAuth2RedirectURL = ""
|
||||
defaultOAuth2Provider = ""
|
||||
)
|
||||
|
||||
// Config manages configuration parameters.
|
||||
type Config struct {
|
||||
IsHTTPS bool
|
||||
baseURL string
|
||||
rootURL string
|
||||
basePath string
|
||||
}
|
||||
|
||||
func (c *Config) parseBaseURL() {
|
||||
baseURL := os.Getenv("BASE_URL")
|
||||
if baseURL == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if baseURL[len(baseURL)-1:] == "/" {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
logger.Error("Invalid BASE_URL: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(u.Scheme)
|
||||
if scheme != "https" && scheme != "http" {
|
||||
logger.Error("Invalid BASE_URL: scheme must be http or https")
|
||||
return
|
||||
}
|
||||
|
||||
c.baseURL = baseURL
|
||||
c.basePath = u.Path
|
||||
|
||||
u.Path = ""
|
||||
c.rootURL = u.String()
|
||||
}
|
||||
|
||||
// HasDebugMode returns true if debug mode is enabled.
|
||||
func (c *Config) HasDebugMode() bool {
|
||||
return getBooleanValue("DEBUG")
|
||||
}
|
||||
|
||||
// BaseURL returns the application base URL with path.
|
||||
func (c *Config) BaseURL() string {
|
||||
return c.baseURL
|
||||
}
|
||||
|
||||
// RootURL returns the base URL without path.
|
||||
func (c *Config) RootURL() string {
|
||||
return c.rootURL
|
||||
}
|
||||
|
||||
// BasePath returns the application base path according to the base URL.
|
||||
func (c *Config) BasePath() string {
|
||||
return c.basePath
|
||||
}
|
||||
|
||||
// DatabaseURL returns the database URL.
|
||||
func (c *Config) DatabaseURL() string {
|
||||
value, exists := os.LookupEnv("DATABASE_URL")
|
||||
if !exists {
|
||||
logger.Info("The environment variable DATABASE_URL is not configured (the default value is used instead)")
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
value = defaultDatabaseURL
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// DatabaseMaxConns returns the maximum number of database connections.
|
||||
func (c *Config) DatabaseMaxConns() int {
|
||||
return getIntValue("DATABASE_MAX_CONNS", defaultDatabaseMaxConns)
|
||||
}
|
||||
|
||||
// DatabaseMinConns returns the minimum number of database connections.
|
||||
func (c *Config) DatabaseMinConns() int {
|
||||
return getIntValue("DATABASE_MIN_CONNS", defaultDatabaseMinConns)
|
||||
}
|
||||
|
||||
// ListenAddr returns the listen address for the HTTP server.
|
||||
func (c *Config) ListenAddr() string {
|
||||
if port := os.Getenv("PORT"); port != "" {
|
||||
return ":" + port
|
||||
}
|
||||
|
||||
return getStringValue("LISTEN_ADDR", defaultListenAddr)
|
||||
}
|
||||
|
||||
// CertFile returns the SSL certificate filename if any.
|
||||
func (c *Config) CertFile() string {
|
||||
return getStringValue("CERT_FILE", defaultCertFile)
|
||||
}
|
||||
|
||||
// KeyFile returns the private key filename for custom SSL certificate.
|
||||
func (c *Config) KeyFile() string {
|
||||
return getStringValue("KEY_FILE", defaultKeyFile)
|
||||
}
|
||||
|
||||
// CertDomain returns the domain to use for Let's Encrypt certificate.
|
||||
func (c *Config) CertDomain() string {
|
||||
return getStringValue("CERT_DOMAIN", defaultCertDomain)
|
||||
}
|
||||
|
||||
// CertCache returns the directory to use for Let's Encrypt session cache.
|
||||
func (c *Config) CertCache() string {
|
||||
return getStringValue("CERT_CACHE", defaultCertCache)
|
||||
}
|
||||
|
||||
// CleanupFrequency returns the interval for cleanup jobs.
|
||||
func (c *Config) CleanupFrequency() int {
|
||||
return getIntValue("CLEANUP_FREQUENCY", defaultCleanupFrequency)
|
||||
}
|
||||
|
||||
// WorkerPoolSize returns the number of background worker.
|
||||
func (c *Config) WorkerPoolSize() int {
|
||||
return getIntValue("WORKER_POOL_SIZE", defaultWorkerPoolSize)
|
||||
}
|
||||
|
||||
// PollingFrequency returns the interval to refresh feeds in the background.
|
||||
func (c *Config) PollingFrequency() int {
|
||||
return getIntValue("POLLING_FREQUENCY", defaultPollingFrequency)
|
||||
}
|
||||
|
||||
// BatchSize returns the number of feeds to send for background processing.
|
||||
func (c *Config) BatchSize() int {
|
||||
return getIntValue("BATCH_SIZE", defaultBatchSize)
|
||||
}
|
||||
|
||||
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
|
||||
func (c *Config) IsOAuth2UserCreationAllowed() bool {
|
||||
return getBooleanValue("OAUTH2_USER_CREATION")
|
||||
}
|
||||
|
||||
// OAuth2ClientID returns the OAuth2 Client ID.
|
||||
func (c *Config) OAuth2ClientID() string {
|
||||
return getStringValue("OAUTH2_CLIENT_ID", defaultOAuth2ClientID)
|
||||
}
|
||||
|
||||
// OAuth2ClientSecret returns the OAuth2 client secret.
|
||||
func (c *Config) OAuth2ClientSecret() string {
|
||||
return getStringValue("OAUTH2_CLIENT_SECRET", defaultOAuth2ClientSecret)
|
||||
}
|
||||
|
||||
// OAuth2RedirectURL returns the OAuth2 redirect URL.
|
||||
func (c *Config) OAuth2RedirectURL() string {
|
||||
return getStringValue("OAUTH2_REDIRECT_URL", defaultOAuth2RedirectURL)
|
||||
}
|
||||
|
||||
// OAuth2Provider returns the name of the OAuth2 provider configured.
|
||||
func (c *Config) OAuth2Provider() string {
|
||||
return getStringValue("OAUTH2_PROVIDER", defaultOAuth2Provider)
|
||||
}
|
||||
|
||||
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
|
||||
func (c *Config) HasHSTS() bool {
|
||||
return !getBooleanValue("DISABLE_HSTS")
|
||||
}
|
||||
|
||||
// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty.
|
||||
func (c *Config) RunMigrations() bool {
|
||||
return getBooleanValue("RUN_MIGRATIONS")
|
||||
}
|
||||
|
||||
// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty.
|
||||
func (c *Config) CreateAdmin() bool {
|
||||
return getBooleanValue("CREATE_ADMIN")
|
||||
}
|
||||
|
||||
// PocketConsumerKey returns the Pocket Consumer Key if defined as environment variable.
|
||||
func (c *Config) PocketConsumerKey(defaultValue string) string {
|
||||
return getStringValue("POCKET_CONSUMER_KEY", defaultValue)
|
||||
}
|
||||
|
||||
// ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
||||
func (c *Config) ProxyImages() string {
|
||||
return getStringValue("PROXY_IMAGES", defaultProxyImages)
|
||||
}
|
||||
|
||||
// HasHTTPService returns true if the HTTP service is enabled.
|
||||
func (c *Config) HasHTTPService() bool {
|
||||
return !getBooleanValue("DISABLE_HTTP_SERVICE")
|
||||
}
|
||||
|
||||
// HasSchedulerService returns true if the scheduler service is enabled.
|
||||
func (c *Config) HasSchedulerService() bool {
|
||||
return !getBooleanValue("DISABLE_SCHEDULER_SERVICE")
|
||||
}
|
||||
|
||||
// ArchiveReadDays returns the number of days after which marking read items as removed.
|
||||
func (c *Config) ArchiveReadDays() int {
|
||||
return getIntValue("ARCHIVE_READ_DAYS", defaultArchiveReadDays)
|
||||
}
|
||||
|
||||
// NewConfig returns a new Config.
|
||||
func NewConfig() *Config {
|
||||
cfg := &Config{
|
||||
baseURL: defaultBaseURL,
|
||||
rootURL: defaultBaseURL,
|
||||
IsHTTPS: getBooleanValue("HTTPS"),
|
||||
}
|
||||
|
||||
cfg.parseBaseURL()
|
||||
return cfg
|
||||
}
|
||||
|
||||
func getBooleanValue(key string) bool {
|
||||
value := strings.ToLower(os.Getenv(key))
|
||||
if value == "1" || value == "yes" || value == "true" || value == "on" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getStringValue(key, fallback string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func getIntValue(key string, fallback int) int {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
// Opts holds parsed configuration options.
|
||||
var Opts *Options
|
||||
|
||||
+867
-274
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,10 +1,10 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Copyright 2019 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package config handles configuration values for the application.
|
||||
Package config handles configuration management for the application.
|
||||
|
||||
*/
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
@@ -0,0 +1,538 @@
|
||||
// Copyright 2019 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/version"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHTTPS = false
|
||||
defaultLogDateTime = false
|
||||
defaultHSTS = true
|
||||
defaultHTTPService = true
|
||||
defaultSchedulerService = true
|
||||
defaultDebug = false
|
||||
defaultTiming = false
|
||||
defaultBaseURL = "http://localhost"
|
||||
defaultRootURL = "http://localhost"
|
||||
defaultBasePath = ""
|
||||
defaultWorkerPoolSize = 5
|
||||
defaultPollingFrequency = 60
|
||||
defaultBatchSize = 100
|
||||
defaultPollingScheduler = "round_robin"
|
||||
defaultSchedulerEntryFrequencyMinInterval = 5
|
||||
defaultSchedulerEntryFrequencyMaxInterval = 24 * 60
|
||||
defaultPollingParsingErrorLimit = 3
|
||||
defaultRunMigrations = false
|
||||
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
|
||||
defaultDatabaseMaxConns = 20
|
||||
defaultDatabaseMinConns = 1
|
||||
defaultListenAddr = "127.0.0.1:8080"
|
||||
defaultCertFile = ""
|
||||
defaultKeyFile = ""
|
||||
defaultCertDomain = ""
|
||||
defaultCleanupFrequencyHours = 24
|
||||
defaultCleanupArchiveReadDays = 60
|
||||
defaultCleanupArchiveUnreadDays = 180
|
||||
defaultCleanupRemoveSessionsDays = 30
|
||||
defaultProxyImages = "http-only"
|
||||
defaultFetchYouTubeWatchTime = false
|
||||
defaultCreateAdmin = false
|
||||
defaultAdminUsername = ""
|
||||
defaultAdminPassword = ""
|
||||
defaultOAuth2UserCreation = false
|
||||
defaultOAuth2ClientID = ""
|
||||
defaultOAuth2ClientSecret = ""
|
||||
defaultOAuth2RedirectURL = ""
|
||||
defaultOAuth2OidcDiscoveryEndpoint = ""
|
||||
defaultOAuth2Provider = ""
|
||||
defaultPocketConsumerKey = ""
|
||||
defaultHTTPClientTimeout = 20
|
||||
defaultHTTPClientMaxBodySize = 15
|
||||
defaultHTTPClientProxy = ""
|
||||
defaultAuthProxyHeader = ""
|
||||
defaultAuthProxyUserCreation = false
|
||||
defaultMaintenanceMode = false
|
||||
defaultMaintenanceMessage = "Miniflux is currently under maintenance"
|
||||
defaultMetricsCollector = false
|
||||
defaultMetricsRefreshInterval = 60
|
||||
defaultMetricsAllowedNetworks = "127.0.0.1/8"
|
||||
)
|
||||
|
||||
var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)"
|
||||
|
||||
// Option contains a key to value map of a single option. It may be used to output debug strings.
|
||||
type Option struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// Options contains configuration options.
|
||||
type Options struct {
|
||||
HTTPS bool
|
||||
logDateTime bool
|
||||
hsts bool
|
||||
httpService bool
|
||||
schedulerService bool
|
||||
debug bool
|
||||
serverTimingHeader bool
|
||||
baseURL string
|
||||
rootURL string
|
||||
basePath string
|
||||
databaseURL string
|
||||
databaseMaxConns int
|
||||
databaseMinConns int
|
||||
runMigrations bool
|
||||
listenAddr string
|
||||
certFile string
|
||||
certDomain string
|
||||
certKeyFile string
|
||||
cleanupFrequencyHours int
|
||||
cleanupArchiveReadDays int
|
||||
cleanupArchiveUnreadDays int
|
||||
cleanupRemoveSessionsDays int
|
||||
pollingFrequency int
|
||||
batchSize int
|
||||
pollingScheduler string
|
||||
schedulerEntryFrequencyMinInterval int
|
||||
schedulerEntryFrequencyMaxInterval int
|
||||
pollingParsingErrorLimit int
|
||||
workerPoolSize int
|
||||
createAdmin bool
|
||||
adminUsername string
|
||||
adminPassword string
|
||||
proxyImages string
|
||||
fetchYouTubeWatchTime bool
|
||||
oauth2UserCreationAllowed bool
|
||||
oauth2ClientID string
|
||||
oauth2ClientSecret string
|
||||
oauth2RedirectURL string
|
||||
oauth2OidcDiscoveryEndpoint string
|
||||
oauth2Provider string
|
||||
pocketConsumerKey string
|
||||
httpClientTimeout int
|
||||
httpClientMaxBodySize int64
|
||||
httpClientProxy string
|
||||
httpClientUserAgent string
|
||||
authProxyHeader string
|
||||
authProxyUserCreation bool
|
||||
maintenanceMode bool
|
||||
maintenanceMessage string
|
||||
metricsCollector bool
|
||||
metricsRefreshInterval int
|
||||
metricsAllowedNetworks []string
|
||||
}
|
||||
|
||||
// NewOptions returns Options with default values.
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
HTTPS: defaultHTTPS,
|
||||
logDateTime: defaultLogDateTime,
|
||||
hsts: defaultHSTS,
|
||||
httpService: defaultHTTPService,
|
||||
schedulerService: defaultSchedulerService,
|
||||
debug: defaultDebug,
|
||||
serverTimingHeader: defaultTiming,
|
||||
baseURL: defaultBaseURL,
|
||||
rootURL: defaultRootURL,
|
||||
basePath: defaultBasePath,
|
||||
databaseURL: defaultDatabaseURL,
|
||||
databaseMaxConns: defaultDatabaseMaxConns,
|
||||
databaseMinConns: defaultDatabaseMinConns,
|
||||
runMigrations: defaultRunMigrations,
|
||||
listenAddr: defaultListenAddr,
|
||||
certFile: defaultCertFile,
|
||||
certDomain: defaultCertDomain,
|
||||
certKeyFile: defaultKeyFile,
|
||||
cleanupFrequencyHours: defaultCleanupFrequencyHours,
|
||||
cleanupArchiveReadDays: defaultCleanupArchiveReadDays,
|
||||
cleanupArchiveUnreadDays: defaultCleanupArchiveUnreadDays,
|
||||
cleanupRemoveSessionsDays: defaultCleanupRemoveSessionsDays,
|
||||
pollingFrequency: defaultPollingFrequency,
|
||||
batchSize: defaultBatchSize,
|
||||
pollingScheduler: defaultPollingScheduler,
|
||||
schedulerEntryFrequencyMinInterval: defaultSchedulerEntryFrequencyMinInterval,
|
||||
schedulerEntryFrequencyMaxInterval: defaultSchedulerEntryFrequencyMaxInterval,
|
||||
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
|
||||
workerPoolSize: defaultWorkerPoolSize,
|
||||
createAdmin: defaultCreateAdmin,
|
||||
proxyImages: defaultProxyImages,
|
||||
fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime,
|
||||
oauth2UserCreationAllowed: defaultOAuth2UserCreation,
|
||||
oauth2ClientID: defaultOAuth2ClientID,
|
||||
oauth2ClientSecret: defaultOAuth2ClientSecret,
|
||||
oauth2RedirectURL: defaultOAuth2RedirectURL,
|
||||
oauth2OidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint,
|
||||
oauth2Provider: defaultOAuth2Provider,
|
||||
pocketConsumerKey: defaultPocketConsumerKey,
|
||||
httpClientTimeout: defaultHTTPClientTimeout,
|
||||
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
|
||||
httpClientProxy: defaultHTTPClientProxy,
|
||||
httpClientUserAgent: defaultHTTPClientUserAgent,
|
||||
authProxyHeader: defaultAuthProxyHeader,
|
||||
authProxyUserCreation: defaultAuthProxyUserCreation,
|
||||
maintenanceMode: defaultMaintenanceMode,
|
||||
maintenanceMessage: defaultMaintenanceMessage,
|
||||
metricsCollector: defaultMetricsCollector,
|
||||
metricsRefreshInterval: defaultMetricsRefreshInterval,
|
||||
metricsAllowedNetworks: []string{defaultMetricsAllowedNetworks},
|
||||
}
|
||||
}
|
||||
|
||||
// LogDateTime returns true if the date/time should be displayed in log messages.
|
||||
func (o *Options) LogDateTime() bool {
|
||||
return o.logDateTime
|
||||
}
|
||||
|
||||
// HasMaintenanceMode returns true if maintenance mode is enabled.
|
||||
func (o *Options) HasMaintenanceMode() bool {
|
||||
return o.maintenanceMode
|
||||
}
|
||||
|
||||
// MaintenanceMessage returns maintenance message.
|
||||
func (o *Options) MaintenanceMessage() string {
|
||||
return o.maintenanceMessage
|
||||
}
|
||||
|
||||
// HasDebugMode returns true if debug mode is enabled.
|
||||
func (o *Options) HasDebugMode() bool {
|
||||
return o.debug
|
||||
}
|
||||
|
||||
// HasServerTimingHeader returns true if server-timing headers enabled.
|
||||
func (o *Options) HasServerTimingHeader() bool {
|
||||
return o.serverTimingHeader
|
||||
}
|
||||
|
||||
// BaseURL returns the application base URL with path.
|
||||
func (o *Options) BaseURL() string {
|
||||
return o.baseURL
|
||||
}
|
||||
|
||||
// RootURL returns the base URL without path.
|
||||
func (o *Options) RootURL() string {
|
||||
return o.rootURL
|
||||
}
|
||||
|
||||
// BasePath returns the application base path according to the base URL.
|
||||
func (o *Options) BasePath() string {
|
||||
return o.basePath
|
||||
}
|
||||
|
||||
// IsDefaultDatabaseURL returns true if the default database URL is used.
|
||||
func (o *Options) IsDefaultDatabaseURL() bool {
|
||||
return o.databaseURL == defaultDatabaseURL
|
||||
}
|
||||
|
||||
// DatabaseURL returns the database URL.
|
||||
func (o *Options) DatabaseURL() string {
|
||||
return o.databaseURL
|
||||
}
|
||||
|
||||
// DatabaseMaxConns returns the maximum number of database connections.
|
||||
func (o *Options) DatabaseMaxConns() int {
|
||||
return o.databaseMaxConns
|
||||
}
|
||||
|
||||
// DatabaseMinConns returns the minimum number of database connections.
|
||||
func (o *Options) DatabaseMinConns() int {
|
||||
return o.databaseMinConns
|
||||
}
|
||||
|
||||
// ListenAddr returns the listen address for the HTTP server.
|
||||
func (o *Options) ListenAddr() string {
|
||||
return o.listenAddr
|
||||
}
|
||||
|
||||
// CertFile returns the SSL certificate filename if any.
|
||||
func (o *Options) CertFile() string {
|
||||
return o.certFile
|
||||
}
|
||||
|
||||
// CertKeyFile returns the private key filename for custom SSL certificate.
|
||||
func (o *Options) CertKeyFile() string {
|
||||
return o.certKeyFile
|
||||
}
|
||||
|
||||
// CertDomain returns the domain to use for Let's Encrypt certificate.
|
||||
func (o *Options) CertDomain() string {
|
||||
return o.certDomain
|
||||
}
|
||||
|
||||
// CleanupFrequencyHours returns the interval in hours for cleanup jobs.
|
||||
func (o *Options) CleanupFrequencyHours() int {
|
||||
return o.cleanupFrequencyHours
|
||||
}
|
||||
|
||||
// CleanupArchiveReadDays returns the number of days after which marking read items as removed.
|
||||
func (o *Options) CleanupArchiveReadDays() int {
|
||||
return o.cleanupArchiveReadDays
|
||||
}
|
||||
|
||||
// CleanupArchiveUnreadDays returns the number of days after which marking unread items as removed.
|
||||
func (o *Options) CleanupArchiveUnreadDays() int {
|
||||
return o.cleanupArchiveUnreadDays
|
||||
}
|
||||
|
||||
// CleanupRemoveSessionsDays returns the number of days after which to remove sessions.
|
||||
func (o *Options) CleanupRemoveSessionsDays() int {
|
||||
return o.cleanupRemoveSessionsDays
|
||||
}
|
||||
|
||||
// WorkerPoolSize returns the number of background worker.
|
||||
func (o *Options) WorkerPoolSize() int {
|
||||
return o.workerPoolSize
|
||||
}
|
||||
|
||||
// PollingFrequency returns the interval to refresh feeds in the background.
|
||||
func (o *Options) PollingFrequency() int {
|
||||
return o.pollingFrequency
|
||||
}
|
||||
|
||||
// BatchSize returns the number of feeds to send for background processing.
|
||||
func (o *Options) BatchSize() int {
|
||||
return o.batchSize
|
||||
}
|
||||
|
||||
// PollingScheduler returns the scheduler used for polling feeds.
|
||||
func (o *Options) PollingScheduler() string {
|
||||
return o.pollingScheduler
|
||||
}
|
||||
|
||||
// SchedulerEntryFrequencyMaxInterval returns the maximum interval in minutes for the entry frequency scheduler.
|
||||
func (o *Options) SchedulerEntryFrequencyMaxInterval() int {
|
||||
return o.schedulerEntryFrequencyMaxInterval
|
||||
}
|
||||
|
||||
// SchedulerEntryFrequencyMinInterval returns the minimum interval in minutes for the entry frequency scheduler.
|
||||
func (o *Options) SchedulerEntryFrequencyMinInterval() int {
|
||||
return o.schedulerEntryFrequencyMinInterval
|
||||
}
|
||||
|
||||
// PollingParsingErrorLimit returns the limit of errors when to stop polling.
|
||||
func (o *Options) PollingParsingErrorLimit() int {
|
||||
return o.pollingParsingErrorLimit
|
||||
}
|
||||
|
||||
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
|
||||
func (o *Options) IsOAuth2UserCreationAllowed() bool {
|
||||
return o.oauth2UserCreationAllowed
|
||||
}
|
||||
|
||||
// OAuth2ClientID returns the OAuth2 Client ID.
|
||||
func (o *Options) OAuth2ClientID() string {
|
||||
return o.oauth2ClientID
|
||||
}
|
||||
|
||||
// OAuth2ClientSecret returns the OAuth2 client secret.
|
||||
func (o *Options) OAuth2ClientSecret() string {
|
||||
return o.oauth2ClientSecret
|
||||
}
|
||||
|
||||
// OAuth2RedirectURL returns the OAuth2 redirect URL.
|
||||
func (o *Options) OAuth2RedirectURL() string {
|
||||
return o.oauth2RedirectURL
|
||||
}
|
||||
|
||||
// OAuth2OidcDiscoveryEndpoint returns the OAuth2 OIDC discovery endpoint.
|
||||
func (o *Options) OAuth2OidcDiscoveryEndpoint() string {
|
||||
return o.oauth2OidcDiscoveryEndpoint
|
||||
}
|
||||
|
||||
// OAuth2Provider returns the name of the OAuth2 provider configured.
|
||||
func (o *Options) OAuth2Provider() string {
|
||||
return o.oauth2Provider
|
||||
}
|
||||
|
||||
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
|
||||
func (o *Options) HasHSTS() bool {
|
||||
return o.hsts
|
||||
}
|
||||
|
||||
// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty.
|
||||
func (o *Options) RunMigrations() bool {
|
||||
return o.runMigrations
|
||||
}
|
||||
|
||||
// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty.
|
||||
func (o *Options) CreateAdmin() bool {
|
||||
return o.createAdmin
|
||||
}
|
||||
|
||||
// AdminUsername returns the admin username if defined.
|
||||
func (o *Options) AdminUsername() string {
|
||||
return o.adminUsername
|
||||
}
|
||||
|
||||
// AdminPassword returns the admin password if defined.
|
||||
func (o *Options) AdminPassword() string {
|
||||
return o.adminPassword
|
||||
}
|
||||
|
||||
// FetchYouTubeWatchTime returns true if the YouTube video duration
|
||||
// should be fetched and used as a reading time.
|
||||
func (o *Options) FetchYouTubeWatchTime() bool {
|
||||
return o.fetchYouTubeWatchTime
|
||||
}
|
||||
|
||||
// ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
||||
func (o *Options) ProxyImages() string {
|
||||
return o.proxyImages
|
||||
}
|
||||
|
||||
// HasHTTPService returns true if the HTTP service is enabled.
|
||||
func (o *Options) HasHTTPService() bool {
|
||||
return o.httpService
|
||||
}
|
||||
|
||||
// HasSchedulerService returns true if the scheduler service is enabled.
|
||||
func (o *Options) HasSchedulerService() bool {
|
||||
return o.schedulerService
|
||||
}
|
||||
|
||||
// PocketConsumerKey returns the Pocket Consumer Key if configured.
|
||||
func (o *Options) PocketConsumerKey(defaultValue string) string {
|
||||
if o.pocketConsumerKey != "" {
|
||||
return o.pocketConsumerKey
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// HTTPClientTimeout returns the time limit in seconds before the HTTP client cancel the request.
|
||||
func (o *Options) HTTPClientTimeout() int {
|
||||
return o.httpClientTimeout
|
||||
}
|
||||
|
||||
// HTTPClientMaxBodySize returns the number of bytes allowed for the HTTP client to transfer.
|
||||
func (o *Options) HTTPClientMaxBodySize() int64 {
|
||||
return o.httpClientMaxBodySize
|
||||
}
|
||||
|
||||
// HTTPClientProxy returns the proxy URL for HTTP client.
|
||||
func (o *Options) HTTPClientProxy() string {
|
||||
return o.httpClientProxy
|
||||
}
|
||||
|
||||
// HasHTTPClientProxyConfigured returns true if the HTTP proxy is configured.
|
||||
func (o *Options) HasHTTPClientProxyConfigured() bool {
|
||||
return o.httpClientProxy != ""
|
||||
}
|
||||
|
||||
// AuthProxyHeader returns an HTTP header name that contains username for
|
||||
// authentication using auth proxy.
|
||||
func (o *Options) AuthProxyHeader() string {
|
||||
return o.authProxyHeader
|
||||
}
|
||||
|
||||
// IsAuthProxyUserCreationAllowed returns true if user creation is allowed for
|
||||
// users authenticated using auth proxy.
|
||||
func (o *Options) IsAuthProxyUserCreationAllowed() bool {
|
||||
return o.authProxyUserCreation
|
||||
}
|
||||
|
||||
// HasMetricsCollector returns true if metrics collection is enabled.
|
||||
func (o *Options) HasMetricsCollector() bool {
|
||||
return o.metricsCollector
|
||||
}
|
||||
|
||||
// MetricsRefreshInterval returns the refresh interval in seconds.
|
||||
func (o *Options) MetricsRefreshInterval() int {
|
||||
return o.metricsRefreshInterval
|
||||
}
|
||||
|
||||
// MetricsAllowedNetworks returns the list of networks allowed to connect to the metrics endpoint.
|
||||
func (o *Options) MetricsAllowedNetworks() []string {
|
||||
return o.metricsAllowedNetworks
|
||||
}
|
||||
|
||||
// HTTPClientUserAgent returns the global User-Agent header for miniflux.
|
||||
func (o *Options) HTTPClientUserAgent() string {
|
||||
return o.httpClientUserAgent
|
||||
}
|
||||
|
||||
// SortedOptions returns options as a list of key value pairs, sorted by keys.
|
||||
func (o *Options) SortedOptions() []*Option {
|
||||
var keyValues = map[string]interface{}{
|
||||
"ADMIN_PASSWORD": o.adminPassword,
|
||||
"ADMIN_USERNAME": o.adminUsername,
|
||||
"AUTH_PROXY_HEADER": o.authProxyHeader,
|
||||
"AUTH_PROXY_USER_CREATION": o.authProxyUserCreation,
|
||||
"BASE_PATH": o.basePath,
|
||||
"BASE_URL": o.baseURL,
|
||||
"BATCH_SIZE": o.batchSize,
|
||||
"CERT_DOMAIN": o.certDomain,
|
||||
"CERT_FILE": o.certFile,
|
||||
"CLEANUP_ARCHIVE_READ_DAYS": o.cleanupArchiveReadDays,
|
||||
"CLEANUP_ARCHIVE_UNREAD_DAYS": o.cleanupArchiveUnreadDays,
|
||||
"CLEANUP_FREQUENCY_HOURS": o.cleanupFrequencyHours,
|
||||
"CLEANUP_REMOVE_SESSIONS_DAYS": o.cleanupRemoveSessionsDays,
|
||||
"CREATE_ADMIN": o.createAdmin,
|
||||
"DATABASE_MAX_CONNS": o.databaseMaxConns,
|
||||
"DATABASE_MIN_CONNS": o.databaseMinConns,
|
||||
"DATABASE_URL": o.databaseURL,
|
||||
"DEBUG": o.debug,
|
||||
"FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime,
|
||||
"HSTS": o.hsts,
|
||||
"HTTPS": o.HTTPS,
|
||||
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
|
||||
"HTTP_CLIENT_PROXY": o.httpClientProxy,
|
||||
"HTTP_CLIENT_TIMEOUT": o.httpClientTimeout,
|
||||
"HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent,
|
||||
"HTTP_SERVICE": o.httpService,
|
||||
"KEY_FILE": o.certKeyFile,
|
||||
"LISTEN_ADDR": o.listenAddr,
|
||||
"LOG_DATE_TIME": o.logDateTime,
|
||||
"MAINTENANCE_MESSAGE": o.maintenanceMessage,
|
||||
"MAINTENANCE_MODE": o.maintenanceMode,
|
||||
"METRICS_ALLOWED_NETWORKS": o.metricsAllowedNetworks,
|
||||
"METRICS_COLLECTOR": o.metricsCollector,
|
||||
"METRICS_REFRESH_INTERVAL": o.metricsRefreshInterval,
|
||||
"OAUTH2_CLIENT_ID": o.oauth2ClientID,
|
||||
"OAUTH2_CLIENT_SECRET": o.oauth2ClientSecret,
|
||||
"OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oauth2OidcDiscoveryEndpoint,
|
||||
"OAUTH2_PROVIDER": o.oauth2Provider,
|
||||
"OAUTH2_REDIRECT_URL": o.oauth2RedirectURL,
|
||||
"OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed,
|
||||
"POCKET_CONSUMER_KEY": o.pocketConsumerKey,
|
||||
"POLLING_FREQUENCY": o.pollingFrequency,
|
||||
"POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit,
|
||||
"POLLING_SCHEDULER": o.pollingScheduler,
|
||||
"PROXY_IMAGES": o.proxyImages,
|
||||
"ROOT_URL": o.rootURL,
|
||||
"RUN_MIGRATIONS": o.runMigrations,
|
||||
"SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval,
|
||||
"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval,
|
||||
"SCHEDULER_SERVICE": o.schedulerService,
|
||||
"SERVER_TIMING_HEADER": o.serverTimingHeader,
|
||||
"WORKER_POOL_SIZE": o.workerPoolSize,
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(keyValues))
|
||||
for key := range keyValues {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var sortedOptions []*Option
|
||||
for _, key := range keys {
|
||||
sortedOptions = append(sortedOptions, &Option{Key: key, Value: keyValues[key]})
|
||||
}
|
||||
return sortedOptions
|
||||
}
|
||||
|
||||
func (o *Options) String() string {
|
||||
var builder strings.Builder
|
||||
|
||||
for _, option := range o.SortedOptions() {
|
||||
builder.WriteString(fmt.Sprintf("%s: %v\n", option.Key, option.Value))
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
// Copyright 2019 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
url_parser "net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parser handles configuration parsing.
|
||||
type Parser struct {
|
||||
opts *Options
|
||||
}
|
||||
|
||||
// NewParser returns a new Parser.
|
||||
func NewParser() *Parser {
|
||||
return &Parser{
|
||||
opts: NewOptions(),
|
||||
}
|
||||
}
|
||||
|
||||
// ParseEnvironmentVariables loads configuration values from environment variables.
|
||||
func (p *Parser) ParseEnvironmentVariables() (*Options, error) {
|
||||
err := p.parseLines(os.Environ())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.opts, nil
|
||||
}
|
||||
|
||||
// ParseFile loads configuration values from a local file.
|
||||
func (p *Parser) ParseFile(filename string) (*Options, error) {
|
||||
fp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
err = p.parseLines(p.parseFileContent(fp))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.opts, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (p *Parser) parseLines(lines []string) (err error) {
|
||||
var port string
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.SplitN(line, "=", 2)
|
||||
key := strings.TrimSpace(fields[0])
|
||||
value := strings.TrimSpace(fields[1])
|
||||
|
||||
switch key {
|
||||
case "LOG_DATE_TIME":
|
||||
p.opts.logDateTime = parseBool(value, defaultLogDateTime)
|
||||
case "DEBUG":
|
||||
p.opts.debug = parseBool(value, defaultDebug)
|
||||
case "SERVER_TIMING_HEADER":
|
||||
p.opts.serverTimingHeader = parseBool(value, defaultTiming)
|
||||
case "BASE_URL":
|
||||
p.opts.baseURL, p.opts.rootURL, p.opts.basePath, err = parseBaseURL(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "PORT":
|
||||
port = value
|
||||
case "LISTEN_ADDR":
|
||||
p.opts.listenAddr = parseString(value, defaultListenAddr)
|
||||
case "DATABASE_URL":
|
||||
p.opts.databaseURL = parseString(value, defaultDatabaseURL)
|
||||
case "DATABASE_URL_FILE":
|
||||
p.opts.databaseURL = readSecretFile(value, defaultDatabaseURL)
|
||||
case "DATABASE_MAX_CONNS":
|
||||
p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns)
|
||||
case "DATABASE_MIN_CONNS":
|
||||
p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
|
||||
case "RUN_MIGRATIONS":
|
||||
p.opts.runMigrations = parseBool(value, defaultRunMigrations)
|
||||
case "DISABLE_HSTS":
|
||||
p.opts.hsts = !parseBool(value, defaultHSTS)
|
||||
case "HTTPS":
|
||||
p.opts.HTTPS = parseBool(value, defaultHTTPS)
|
||||
case "DISABLE_SCHEDULER_SERVICE":
|
||||
p.opts.schedulerService = !parseBool(value, defaultSchedulerService)
|
||||
case "DISABLE_HTTP_SERVICE":
|
||||
p.opts.httpService = !parseBool(value, defaultHTTPService)
|
||||
case "CERT_FILE":
|
||||
p.opts.certFile = parseString(value, defaultCertFile)
|
||||
case "KEY_FILE":
|
||||
p.opts.certKeyFile = parseString(value, defaultKeyFile)
|
||||
case "CERT_DOMAIN":
|
||||
p.opts.certDomain = parseString(value, defaultCertDomain)
|
||||
case "CLEANUP_FREQUENCY_HOURS":
|
||||
p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
|
||||
case "CLEANUP_ARCHIVE_READ_DAYS":
|
||||
p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays)
|
||||
case "CLEANUP_ARCHIVE_UNREAD_DAYS":
|
||||
p.opts.cleanupArchiveUnreadDays = parseInt(value, defaultCleanupArchiveUnreadDays)
|
||||
case "CLEANUP_REMOVE_SESSIONS_DAYS":
|
||||
p.opts.cleanupRemoveSessionsDays = parseInt(value, defaultCleanupRemoveSessionsDays)
|
||||
case "WORKER_POOL_SIZE":
|
||||
p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
|
||||
case "POLLING_FREQUENCY":
|
||||
p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency)
|
||||
case "BATCH_SIZE":
|
||||
p.opts.batchSize = parseInt(value, defaultBatchSize)
|
||||
case "POLLING_SCHEDULER":
|
||||
p.opts.pollingScheduler = strings.ToLower(parseString(value, defaultPollingScheduler))
|
||||
case "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL":
|
||||
p.opts.schedulerEntryFrequencyMaxInterval = parseInt(value, defaultSchedulerEntryFrequencyMaxInterval)
|
||||
case "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL":
|
||||
p.opts.schedulerEntryFrequencyMinInterval = parseInt(value, defaultSchedulerEntryFrequencyMinInterval)
|
||||
case "POLLING_PARSING_ERROR_LIMIT":
|
||||
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
|
||||
case "PROXY_IMAGES":
|
||||
p.opts.proxyImages = parseString(value, defaultProxyImages)
|
||||
case "CREATE_ADMIN":
|
||||
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
|
||||
case "ADMIN_USERNAME":
|
||||
p.opts.adminUsername = parseString(value, defaultAdminUsername)
|
||||
case "ADMIN_USERNAME_FILE":
|
||||
p.opts.adminUsername = readSecretFile(value, defaultAdminUsername)
|
||||
case "ADMIN_PASSWORD":
|
||||
p.opts.adminPassword = parseString(value, defaultAdminPassword)
|
||||
case "ADMIN_PASSWORD_FILE":
|
||||
p.opts.adminPassword = readSecretFile(value, defaultAdminPassword)
|
||||
case "POCKET_CONSUMER_KEY":
|
||||
p.opts.pocketConsumerKey = parseString(value, defaultPocketConsumerKey)
|
||||
case "POCKET_CONSUMER_KEY_FILE":
|
||||
p.opts.pocketConsumerKey = readSecretFile(value, defaultPocketConsumerKey)
|
||||
case "OAUTH2_USER_CREATION":
|
||||
p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
|
||||
case "OAUTH2_CLIENT_ID":
|
||||
p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID)
|
||||
case "OAUTH2_CLIENT_ID_FILE":
|
||||
p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID)
|
||||
case "OAUTH2_CLIENT_SECRET":
|
||||
p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret)
|
||||
case "OAUTH2_CLIENT_SECRET_FILE":
|
||||
p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret)
|
||||
case "OAUTH2_REDIRECT_URL":
|
||||
p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
|
||||
case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
|
||||
p.opts.oauth2OidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
|
||||
case "OAUTH2_PROVIDER":
|
||||
p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
|
||||
case "HTTP_CLIENT_TIMEOUT":
|
||||
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
|
||||
case "HTTP_CLIENT_MAX_BODY_SIZE":
|
||||
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
|
||||
case "HTTP_CLIENT_PROXY":
|
||||
p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy)
|
||||
case "HTTP_CLIENT_USER_AGENT":
|
||||
p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
|
||||
case "AUTH_PROXY_HEADER":
|
||||
p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader)
|
||||
case "AUTH_PROXY_USER_CREATION":
|
||||
p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation)
|
||||
case "MAINTENANCE_MODE":
|
||||
p.opts.maintenanceMode = parseBool(value, defaultMaintenanceMode)
|
||||
case "MAINTENANCE_MESSAGE":
|
||||
p.opts.maintenanceMessage = parseString(value, defaultMaintenanceMessage)
|
||||
case "METRICS_COLLECTOR":
|
||||
p.opts.metricsCollector = parseBool(value, defaultMetricsCollector)
|
||||
case "METRICS_REFRESH_INTERVAL":
|
||||
p.opts.metricsRefreshInterval = parseInt(value, defaultMetricsRefreshInterval)
|
||||
case "METRICS_ALLOWED_NETWORKS":
|
||||
p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks})
|
||||
case "FETCH_YOUTUBE_WATCH_TIME":
|
||||
p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime)
|
||||
}
|
||||
}
|
||||
|
||||
if port != "" {
|
||||
p.opts.listenAddr = ":" + port
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBaseURL(value string) (string, string, string, error) {
|
||||
if value == "" {
|
||||
return defaultBaseURL, defaultRootURL, "", nil
|
||||
}
|
||||
|
||||
if value[len(value)-1:] == "/" {
|
||||
value = value[:len(value)-1]
|
||||
}
|
||||
|
||||
url, err := url_parser.Parse(value)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("Invalid BASE_URL: %v", err)
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(url.Scheme)
|
||||
if scheme != "https" && scheme != "http" {
|
||||
return "", "", "", errors.New("Invalid BASE_URL: scheme must be http or https")
|
||||
}
|
||||
|
||||
basePath := url.Path
|
||||
url.Path = ""
|
||||
return value, url.String(), basePath, nil
|
||||
}
|
||||
|
||||
func parseBool(value string, fallback bool) bool {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
value = strings.ToLower(value)
|
||||
if value == "1" || value == "yes" || value == "true" || value == "on" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func parseInt(value string, fallback int) int {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func parseString(value string, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func parseStringList(value string, fallback []string) []string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
var strList []string
|
||||
items := strings.Split(value, ",")
|
||||
for _, item := range items {
|
||||
strList = append(strList, strings.TrimSpace(item))
|
||||
}
|
||||
|
||||
return strList
|
||||
}
|
||||
|
||||
func readSecretFile(filename, fallback string) string {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
value := string(bytes.TrimSpace(data))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2019 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseBoolValue(t *testing.T) {
|
||||
scenarios := map[string]bool{
|
||||
"": true,
|
||||
"1": true,
|
||||
"Yes": true,
|
||||
"yes": true,
|
||||
"True": true,
|
||||
"true": true,
|
||||
"on": true,
|
||||
"false": false,
|
||||
"off": false,
|
||||
"invalid": false,
|
||||
}
|
||||
|
||||
for input, expected := range scenarios {
|
||||
result := parseBool(input, true)
|
||||
if result != expected {
|
||||
t.Errorf(`Unexpected result for %q, got %v instead of %v`, input, result, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStringValueWithUnsetVariable(t *testing.T) {
|
||||
if parseString("", "defaultValue") != "defaultValue" {
|
||||
t.Errorf(`Unset variables should returns the default value`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStringValue(t *testing.T) {
|
||||
if parseString("test", "defaultValue") != "test" {
|
||||
t.Errorf(`Defined variables should returns the specified value`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntValueWithUnsetVariable(t *testing.T) {
|
||||
if parseInt("", 42) != 42 {
|
||||
t.Errorf(`Unset variables should returns the default value`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntValueWithInvalidInput(t *testing.T) {
|
||||
if parseInt("invalid integer", 42) != 42 {
|
||||
t.Errorf(`Invalid integer should returns the default value`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntValue(t *testing.T) {
|
||||
if parseInt("2018", 42) != 2018 {
|
||||
t.Errorf(`Defined variables should returns the specified value`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
The contrib directory contains various useful things contributed by the community.
|
||||
|
||||
Community contributions are not officially supported by the maintainers.
|
||||
There is no guarantee whatsoever that anything in this folder works.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
miniflux_linux_user: miniflux
|
||||
miniflux_db_user_name: miniflux_db_user
|
||||
miniflux_db_user_password: miniflux_db_user_password
|
||||
miniflux_db: miniflux_db
|
||||
miniflux_admin_name: admin
|
||||
miniflux_admin_passwort: miniflux_admin_password
|
||||
miniflux_port: 8080
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
- hosts: miniflux
|
||||
roles:
|
||||
- { role: mgrote.miniflux, tags: "miniflux" }
|
||||
@@ -0,0 +1,23 @@
|
||||
## mgrote.miniflux
|
||||
|
||||
### Details
|
||||
Installs and configures Miniflux v2 with ansible
|
||||
|
||||
### Works on...
|
||||
- [x] Ubuntu (>=18.04)
|
||||
|
||||
### Variables and Defaults
|
||||
##### Linux User
|
||||
miniflux_linux_user: miniflux
|
||||
##### DB User
|
||||
miniflux_db_user_name: miniflux_db_user
|
||||
##### DB Password
|
||||
miniflux_db_user_password: qqqqqqqqqqqqq
|
||||
##### Database
|
||||
miniflux_db: miniflux_db
|
||||
##### Username Miniflux Admin
|
||||
miniflux_admin_name: admin
|
||||
##### Password Miniflux Admin
|
||||
miniflux_admin_passwort: hallowelt
|
||||
##### Port for Miniflux Frontend
|
||||
miniflux_port: 8080
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: start_miniflux.service
|
||||
become: yes
|
||||
systemd:
|
||||
name: miniflux
|
||||
state: restarted
|
||||
enabled: yes
|
||||
# wait 15 seconds(for systemd)
|
||||
- name: miniflux_wait
|
||||
wait_for:
|
||||
timeout: 15
|
||||
@@ -0,0 +1,43 @@
|
||||
- name: add Apt-key for miniflux-repo
|
||||
become: yes
|
||||
apt_key:
|
||||
url: https://apt.miniflux.app/KEY.gpg
|
||||
state: present
|
||||
|
||||
- name: add miniflux-repo
|
||||
become: yes
|
||||
apt_repository:
|
||||
repo: 'deb https://apt.miniflux.app/ /'
|
||||
state: present
|
||||
filename: miniflux_repo
|
||||
update_cache: yes
|
||||
|
||||
- name: install miniflux
|
||||
become: yes
|
||||
apt:
|
||||
name: miniflux
|
||||
state: present
|
||||
|
||||
- name: add miniflux linux_user
|
||||
become: yes
|
||||
user:
|
||||
name: "{{ miniflux_linux_user }}"
|
||||
home: "/var/empty"
|
||||
create_home: "no"
|
||||
system: "yes"
|
||||
shell: "/bin/false"
|
||||
|
||||
- name: create directory "/etc/miniflux.d"
|
||||
become: yes
|
||||
file:
|
||||
path: /etc/miniflux.d
|
||||
state: directory
|
||||
|
||||
- name: copy miniflux.conf
|
||||
become: yes
|
||||
template:
|
||||
src: "miniflux.conf"
|
||||
dest: "/etc/miniflux.conf"
|
||||
notify:
|
||||
- start_miniflux.service
|
||||
- miniflux_wait
|
||||
@@ -0,0 +1,18 @@
|
||||
# See https://docs.miniflux.app/
|
||||
|
||||
LISTEN_ADDR=0.0.0.0:{{ miniflux_port }}
|
||||
DATABASE_URL=user={{ miniflux_db_user_name }} password={{ miniflux_db_user_password }} dbname={{ miniflux_db }} sslmode=disable
|
||||
|
||||
POLLING_FREQUENCY=15
|
||||
PROXY_IMAGES=http-only
|
||||
|
||||
# Run SQL migrations automatically:
|
||||
RUN_MIGRATIONS=1
|
||||
|
||||
CREATE_ADMIN=1
|
||||
ADMIN_USERNAME={{ miniflux_admin_name }}
|
||||
ADMIN_PASSWORD={{ miniflux_admin_passwort }}
|
||||
|
||||
POLLING_FREQUENCY=10
|
||||
|
||||
# Options: https://miniflux.app/miniflux.1.html
|
||||
@@ -0,0 +1,2 @@
|
||||
miniflux.example.org
|
||||
reverse_proxy miniflux:8080
|
||||
@@ -0,0 +1,13 @@
|
||||
Docker-Compose Examples
|
||||
=======================
|
||||
|
||||
Here are few Docker Compose examples:
|
||||
|
||||
- `basic.yml`: Basic example
|
||||
- `caddy.yml`: Use Caddy as reverse-proxy with automatic HTTPS
|
||||
- `traefik.yml`: Use Traefik as reverse-proxy with automatic HTTPS
|
||||
|
||||
```bash
|
||||
docker-compose -f basic.yml up -d db
|
||||
docker-compose -f basic.yml up
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
miniflux:
|
||||
image: miniflux/miniflux:latest
|
||||
container_name: miniflux
|
||||
restart: always
|
||||
ports:
|
||||
- "80:8080"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
|
||||
- RUN_MIGRATIONS=1
|
||||
- CREATE_ADMIN=1
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=test123
|
||||
- DEBUG=1
|
||||
# Optional health check:
|
||||
# healthcheck:
|
||||
# test: ["CMD", "/usr/bin/miniflux", "-healthcheck", "auto"]
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "miniflux"]
|
||||
interval: 10s
|
||||
start_period: 30s
|
||||
volumes:
|
||||
miniflux-db:
|
||||
@@ -0,0 +1,43 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2
|
||||
container_name: caddy
|
||||
depends_on:
|
||||
- miniflux
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- $PWD/Caddyfile:/etc/caddy/Caddyfile
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
miniflux:
|
||||
image: miniflux/miniflux:latest
|
||||
container_name: miniflux
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
|
||||
- RUN_MIGRATIONS=1
|
||||
- CREATE_ADMIN=1
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=test123
|
||||
- BASE_URL=https://miniflux.example.org
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "miniflux"]
|
||||
interval: 10s
|
||||
start_period: 30s
|
||||
volumes:
|
||||
miniflux-db:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
@@ -0,0 +1,53 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.3"
|
||||
container_name: traefik
|
||||
command:
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
|
||||
- "--certificatesresolvers.myresolver.acme.email=postmaster@example.com"
|
||||
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
|
||||
depends_on:
|
||||
- miniflux
|
||||
ports:
|
||||
- "443:443"
|
||||
volumes:
|
||||
- "./letsencrypt:/letsencrypt"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
miniflux:
|
||||
image: miniflux/miniflux:latest
|
||||
container_name: miniflux
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
|
||||
- RUN_MIGRATIONS=1
|
||||
- CREATE_ADMIN=1
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=test123
|
||||
- BASE_URL=https://miniflux.example.org
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.miniflux.rule=Host(`miniflux.example.org`)"
|
||||
- "traefik.http.routers.miniflux.entrypoints=websecure"
|
||||
- "traefik.http.routers.miniflux.tls.certresolver=myresolver"
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "miniflux"]
|
||||
interval: 10s
|
||||
start_period: 30s
|
||||
volumes:
|
||||
miniflux-db:
|
||||
@@ -0,0 +1 @@
|
||||
Grafana Dashboard for Miniflux
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
@@ -36,3 +37,8 @@ func GenerateRandomBytes(size int) []byte {
|
||||
func GenerateRandomString(size int) string {
|
||||
return base64.URLEncoding.EncodeToString(GenerateRandomBytes(size))
|
||||
}
|
||||
|
||||
// GenerateRandomStringHex returns a random hexadecimal string.
|
||||
func GenerateRandomStringHex(size int) string {
|
||||
return hex.EncodeToString(GenerateRandomBytes(size))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package database // import "miniflux.app/database"
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
// Postgresql driver import
|
||||
_ "github.com/lib/pq"
|
||||
@@ -23,3 +24,53 @@ func NewConnectionPool(dsn string, minConnections, maxConnections int) (*sql.DB,
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// Migrate executes database migrations.
|
||||
func Migrate(db *sql.DB) error {
|
||||
var currentVersion int
|
||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||
|
||||
fmt.Println("-> Current schema version:", currentVersion)
|
||||
fmt.Println("-> Latest schema version:", schemaVersion)
|
||||
|
||||
for version := currentVersion; version < schemaVersion; version++ {
|
||||
newVersion := version + 1
|
||||
fmt.Println("* Migrating to version:", newVersion)
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if err := migrations[version](tx); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM schema_version`); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES ($1)`, newVersion); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSchemaUpToDate checks if the database schema is up to date.
|
||||
func IsSchemaUpToDate(db *sql.DB) error {
|
||||
var currentVersion int
|
||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||
if currentVersion < schemaVersion {
|
||||
return fmt.Errorf(`the database schema is not up to date: current=v%d expected=v%d`, currentVersion, schemaVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package database provides functions related to the database connection.
|
||||
|
||||
*/
|
||||
package database // import "miniflux.app/database"
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package database // import "miniflux.app/database"
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"miniflux.app/logger"
|
||||
)
|
||||
|
||||
const schemaVersion = 21
|
||||
|
||||
// Migrate executes database migrations.
|
||||
func Migrate(db *sql.DB) {
|
||||
var currentVersion int
|
||||
db.QueryRow(`select version from schema_version`).Scan(¤tVersion)
|
||||
|
||||
fmt.Println("Current schema version:", currentVersion)
|
||||
fmt.Println("Latest schema version:", schemaVersion)
|
||||
|
||||
for version := currentVersion + 1; version <= schemaVersion; version++ {
|
||||
fmt.Println("Migrating to version:", version)
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
logger.Fatal("[Migrate] %v", err)
|
||||
}
|
||||
|
||||
rawSQL := SqlMap["schema_version_"+strconv.Itoa(version)]
|
||||
// fmt.Println(rawSQL)
|
||||
_, err = tx.Exec(rawSQL)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
logger.Fatal("[Migrate] %v", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`delete from schema_version`); err != nil {
|
||||
tx.Rollback()
|
||||
logger.Fatal("[Migrate] %v", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`insert into schema_version (version) values($1)`, version); err != nil {
|
||||
tx.Rollback()
|
||||
logger.Fatal("[Migrate] %v", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
logger.Fatal("[Migrate] %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
// Copyright 2020 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package database // import "miniflux.app/database"
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
var schemaVersion = len(migrations)
|
||||
|
||||
// Order is important. Add new migrations at the end of the list.
|
||||
var migrations = []func(tx *sql.Tx) error{
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE schema_version (
|
||||
version text not null
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id serial not null,
|
||||
username text not null unique,
|
||||
password text,
|
||||
is_admin bool default 'f',
|
||||
language text default 'en_US',
|
||||
timezone text default 'UTC',
|
||||
theme text default 'default',
|
||||
last_login_at timestamp with time zone,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id serial not null,
|
||||
user_id int not null,
|
||||
token text not null unique,
|
||||
created_at timestamp with time zone default now(),
|
||||
user_agent text,
|
||||
ip text,
|
||||
primary key (id),
|
||||
unique (user_id, token),
|
||||
foreign key (user_id) references users(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE categories (
|
||||
id serial not null,
|
||||
user_id int not null,
|
||||
title text not null,
|
||||
primary key (id),
|
||||
unique (user_id, title),
|
||||
foreign key (user_id) references users(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE feeds (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
category_id int not null,
|
||||
title text not null,
|
||||
feed_url text not null,
|
||||
site_url text not null,
|
||||
checked_at timestamp with time zone default now(),
|
||||
etag_header text default '',
|
||||
last_modified_header text default '',
|
||||
parsing_error_msg text default '',
|
||||
parsing_error_count int default 0,
|
||||
primary key (id),
|
||||
unique (user_id, feed_url),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (category_id) references categories(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TYPE entry_status as enum('unread', 'read', 'removed');
|
||||
|
||||
CREATE TABLE entries (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
feed_id bigint not null,
|
||||
hash text not null,
|
||||
published_at timestamp with time zone not null,
|
||||
title text not null,
|
||||
url text not null,
|
||||
author text,
|
||||
content text,
|
||||
status entry_status default 'unread',
|
||||
primary key (id),
|
||||
unique (feed_id, hash),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (feed_id) references feeds(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE INDEX entries_feed_idx on entries using btree(feed_id);
|
||||
|
||||
CREATE TABLE enclosures (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
entry_id bigint not null,
|
||||
url text not null,
|
||||
size int default 0,
|
||||
mime_type text default '',
|
||||
primary key (id),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (entry_id) references entries(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE icons (
|
||||
id bigserial not null,
|
||||
hash text not null unique,
|
||||
mime_type text not null,
|
||||
content bytea not null,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
CREATE TABLE feed_icons (
|
||||
feed_id bigint not null,
|
||||
icon_id bigint not null,
|
||||
primary key(feed_id, icon_id),
|
||||
foreign key (feed_id) references feeds(id) on delete cascade,
|
||||
foreign key (icon_id) references icons(id) on delete cascade
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE EXTENSION IF NOT EXISTS hstore;
|
||||
ALTER TABLE users ADD COLUMN extra hstore;
|
||||
CREATE INDEX users_extra_idx ON users using gin(extra);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE tokens (
|
||||
id text not null,
|
||||
value text not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
primary key(id, value)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TYPE entry_sorting_direction AS enum('asc', 'desc');
|
||||
ALTER TABLE users ADD COLUMN entry_direction entry_sorting_direction default 'asc';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE integrations (
|
||||
user_id int not null,
|
||||
pinboard_enabled bool default 'f',
|
||||
pinboard_token text default '',
|
||||
pinboard_tags text default 'miniflux',
|
||||
pinboard_mark_as_unread bool default 'f',
|
||||
instapaper_enabled bool default 'f',
|
||||
instapaper_username text default '',
|
||||
instapaper_password text default '',
|
||||
fever_enabled bool default 'f',
|
||||
fever_username text default '',
|
||||
fever_password text default '',
|
||||
fever_token text default '',
|
||||
primary key(user_id)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN scraper_rules text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN rewrite_rules text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN crawler boolean default 'f'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE sessions rename to user_sessions`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
DROP TABLE tokens;
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id text not null,
|
||||
data jsonb not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
primary key(id)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_client_id text default '';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_client_secret text default '';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_username text default '';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_password text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE entries ADD COLUMN starred bool default 'f'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE INDEX entries_user_status_idx ON entries(user_id, status);
|
||||
CREATE INDEX feeds_user_category_idx ON feeds(user_id, category_id);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN nunux_keeper_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN nunux_keeper_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN nunux_keeper_api_key text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE enclosures ALTER COLUMN size SET DATA TYPE bigint`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE entries ADD COLUMN comments_url text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN pocket_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN pocket_access_token text default '';
|
||||
ALTER TABLE integrations ADD COLUMN pocket_consumer_key text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE user_sessions ALTER COLUMN ip SET DATA TYPE inet using ip::inet;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE feeds ADD COLUMN username text default '';
|
||||
ALTER TABLE feeds ADD COLUMN password text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN document_vectors tsvector;
|
||||
UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
|
||||
CREATE INDEX document_vectors_idx ON entries USING gin(document_vectors);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN user_agent text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
UPDATE
|
||||
entries
|
||||
SET
|
||||
document_vectors = setweight(to_tsvector(substring(coalesce(title, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce(content, '') for 1000000)), 'B')
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN keyboard_shortcuts boolean default 't'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN disabled boolean default 'f';`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE users ALTER COLUMN theme SET DEFAULT 'light_serif';
|
||||
UPDATE users SET theme='light_serif' WHERE theme='default';
|
||||
UPDATE users SET theme='light_sans_serif' WHERE theme='sansserif';
|
||||
UPDATE users SET theme='dark_serif' WHERE theme='black';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN changed_at timestamp with time zone;
|
||||
UPDATE entries SET changed_at = published_at;
|
||||
ALTER TABLE entries ALTER COLUMN changed_at SET not null;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE api_keys (
|
||||
id serial not null,
|
||||
user_id int not null references users(id) on delete cascade,
|
||||
token text not null unique,
|
||||
description text not null,
|
||||
last_used_at timestamp with time zone,
|
||||
created_at timestamp with time zone default now(),
|
||||
primary key(id),
|
||||
unique (user_id, description)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN share_code text not null default '';
|
||||
CREATE UNIQUE INDEX entries_share_code_idx ON entries USING btree(share_code) WHERE share_code <> '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, md5(url))`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE feeds ADD COLUMN next_check_at timestamp with time zone default now();
|
||||
CREATE INDEX entries_user_feed_idx ON entries (user_id, feed_id);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN ignore_http_cache bool default false`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN entries_per_page int default 100`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN show_reading_time boolean default 't'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `CREATE INDEX entries_id_user_status_idx ON entries USING btree (id, user_id, status)`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN fetch_via_proxy bool default false`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `CREATE INDEX entries_feed_id_status_hash_idx ON entries USING btree (feed_id, status, hash)`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `CREATE INDEX entries_user_id_status_starred_idx ON entries (user_id, status, starred)`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN entry_swipe boolean default 't'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE integrations DROP COLUMN fever_password`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE feeds
|
||||
ADD COLUMN blocklist_rules text not null default '',
|
||||
ADD COLUMN keeplist_rules text not null default ''
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE entries ADD COLUMN reading_time int not null default 0`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN created_at timestamp with time zone not null default now();
|
||||
UPDATE entries SET created_at = published_at;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
ALTER TABLE users
|
||||
ADD column stylesheet text not null default '',
|
||||
ADD column google_id text not null default '',
|
||||
ADD column openid_connect_id text not null default ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
DECLARE my_cursor CURSOR FOR
|
||||
SELECT
|
||||
id,
|
||||
COALESCE(extra->'custom_css', '') as custom_css,
|
||||
COALESCE(extra->'google_id', '') as google_id,
|
||||
COALESCE(extra->'oidc_id', '') as oidc_id
|
||||
FROM users
|
||||
FOR UPDATE
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Exec("CLOSE my_cursor")
|
||||
|
||||
for {
|
||||
var (
|
||||
userID int64
|
||||
customStylesheet string
|
||||
googleID string
|
||||
oidcID string
|
||||
)
|
||||
|
||||
if err := tx.QueryRow(`FETCH NEXT FROM my_cursor`).Scan(&userID, &customStylesheet, &googleID, &oidcID); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := tx.Exec(
|
||||
`UPDATE
|
||||
users
|
||||
SET
|
||||
stylesheet=$2,
|
||||
google_id=$3,
|
||||
openid_connect_id=$4
|
||||
WHERE
|
||||
id=$1
|
||||
`,
|
||||
userID, customStylesheet, googleID, oidcID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
ALTER TABLE users DROP COLUMN extra;
|
||||
CREATE UNIQUE INDEX users_google_id_idx ON users(google_id) WHERE google_id <> '';
|
||||
CREATE UNIQUE INDEX users_openid_connect_id_idx ON users(openid_connect_id) WHERE openid_connect_id <> '';
|
||||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
CREATE INDEX entries_feed_url_idx ON entries(feed_id, url);
|
||||
CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
|
||||
CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE acme_cache (
|
||||
key varchar(400) not null primary key,
|
||||
data bytea not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
ALTER TABLE feeds ADD COLUMN allow_self_signed_certificates boolean not null default false
|
||||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TYPE webapp_display_mode AS enum('fullscreen', 'standalone', 'minimal-ui', 'browser');
|
||||
ALTER TABLE users ADD COLUMN display_mode webapp_display_mode default 'standalone';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN cookie text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
}
|
||||
-204
@@ -1,204 +0,0 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
|
||||
package database // import "miniflux.app/database"
|
||||
|
||||
var SqlMap = map[string]string{
|
||||
"schema_version_1": `create table schema_version (
|
||||
version text not null
|
||||
);
|
||||
|
||||
create table users (
|
||||
id serial not null,
|
||||
username text not null unique,
|
||||
password text,
|
||||
is_admin bool default 'f',
|
||||
language text default 'en_US',
|
||||
timezone text default 'UTC',
|
||||
theme text default 'default',
|
||||
last_login_at timestamp with time zone,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
create table sessions (
|
||||
id serial not null,
|
||||
user_id int not null,
|
||||
token text not null unique,
|
||||
created_at timestamp with time zone default now(),
|
||||
user_agent text,
|
||||
ip text,
|
||||
primary key (id),
|
||||
unique (user_id, token),
|
||||
foreign key (user_id) references users(id) on delete cascade
|
||||
);
|
||||
|
||||
create table categories (
|
||||
id serial not null,
|
||||
user_id int not null,
|
||||
title text not null,
|
||||
primary key (id),
|
||||
unique (user_id, title),
|
||||
foreign key (user_id) references users(id) on delete cascade
|
||||
);
|
||||
|
||||
create table feeds (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
category_id int not null,
|
||||
title text not null,
|
||||
feed_url text not null,
|
||||
site_url text not null,
|
||||
checked_at timestamp with time zone default now(),
|
||||
etag_header text default '',
|
||||
last_modified_header text default '',
|
||||
parsing_error_msg text default '',
|
||||
parsing_error_count int default 0,
|
||||
primary key (id),
|
||||
unique (user_id, feed_url),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (category_id) references categories(id) on delete cascade
|
||||
);
|
||||
|
||||
create type entry_status as enum('unread', 'read', 'removed');
|
||||
|
||||
create table entries (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
feed_id bigint not null,
|
||||
hash text not null,
|
||||
published_at timestamp with time zone not null,
|
||||
title text not null,
|
||||
url text not null,
|
||||
author text,
|
||||
content text,
|
||||
status entry_status default 'unread',
|
||||
primary key (id),
|
||||
unique (feed_id, hash),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (feed_id) references feeds(id) on delete cascade
|
||||
);
|
||||
|
||||
create index entries_feed_idx on entries using btree(feed_id);
|
||||
|
||||
create table enclosures (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
entry_id bigint not null,
|
||||
url text not null,
|
||||
size int default 0,
|
||||
mime_type text default '',
|
||||
primary key (id),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (entry_id) references entries(id) on delete cascade
|
||||
);
|
||||
|
||||
create table icons (
|
||||
id bigserial not null,
|
||||
hash text not null unique,
|
||||
mime_type text not null,
|
||||
content bytea not null,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
create table feed_icons (
|
||||
feed_id bigint not null,
|
||||
icon_id bigint not null,
|
||||
primary key(feed_id, icon_id),
|
||||
foreign key (feed_id) references feeds(id) on delete cascade,
|
||||
foreign key (icon_id) references icons(id) on delete cascade
|
||||
);
|
||||
`,
|
||||
"schema_version_10": `drop table tokens;
|
||||
|
||||
create table sessions (
|
||||
id text not null,
|
||||
data jsonb not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
primary key(id)
|
||||
);`,
|
||||
"schema_version_11": `alter table integrations add column wallabag_enabled bool default 'f';
|
||||
alter table integrations add column wallabag_url text default '';
|
||||
alter table integrations add column wallabag_client_id text default '';
|
||||
alter table integrations add column wallabag_client_secret text default '';
|
||||
alter table integrations add column wallabag_username text default '';
|
||||
alter table integrations add column wallabag_password text default '';`,
|
||||
"schema_version_12": `alter table entries add column starred bool default 'f';`,
|
||||
"schema_version_13": `create index entries_user_status_idx on entries(user_id, status);
|
||||
create index feeds_user_category_idx on feeds(user_id, category_id);
|
||||
`,
|
||||
"schema_version_14": `alter table integrations add column nunux_keeper_enabled bool default 'f';
|
||||
alter table integrations add column nunux_keeper_url text default '';
|
||||
alter table integrations add column nunux_keeper_api_key text default '';`,
|
||||
"schema_version_15": `alter table enclosures alter column size set data type bigint;`,
|
||||
"schema_version_16": `alter table entries add column comments_url text default '';`,
|
||||
"schema_version_17": `alter table integrations add column pocket_enabled bool default 'f';
|
||||
alter table integrations add column pocket_access_token text default '';
|
||||
alter table integrations add column pocket_consumer_key text default '';
|
||||
`,
|
||||
"schema_version_18": `alter table user_sessions alter column ip set data type inet using ip::inet;`,
|
||||
"schema_version_19": `alter table feeds add column username text default '';
|
||||
alter table feeds add column password text default '';`,
|
||||
"schema_version_2": `create extension if not exists hstore;
|
||||
alter table users add column extra hstore;
|
||||
create index users_extra_idx on users using gin(extra);
|
||||
`,
|
||||
"schema_version_20": `alter table entries add column document_vectors tsvector;
|
||||
update entries set document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
|
||||
create index document_vectors_idx on entries using gin(document_vectors);`,
|
||||
"schema_version_21": `alter table feeds add column user_agent text default '';`,
|
||||
"schema_version_3": `create table tokens (
|
||||
id text not null,
|
||||
value text not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
primary key(id, value)
|
||||
);`,
|
||||
"schema_version_4": `create type entry_sorting_direction as enum('asc', 'desc');
|
||||
alter table users add column entry_direction entry_sorting_direction default 'asc';
|
||||
`,
|
||||
"schema_version_5": `create table integrations (
|
||||
user_id int not null,
|
||||
pinboard_enabled bool default 'f',
|
||||
pinboard_token text default '',
|
||||
pinboard_tags text default 'miniflux',
|
||||
pinboard_mark_as_unread bool default 'f',
|
||||
instapaper_enabled bool default 'f',
|
||||
instapaper_username text default '',
|
||||
instapaper_password text default '',
|
||||
fever_enabled bool default 'f',
|
||||
fever_username text default '',
|
||||
fever_password text default '',
|
||||
fever_token text default '',
|
||||
primary key(user_id)
|
||||
)
|
||||
`,
|
||||
"schema_version_6": `alter table feeds add column scraper_rules text default '';
|
||||
`,
|
||||
"schema_version_7": `alter table feeds add column rewrite_rules text default '';
|
||||
`,
|
||||
"schema_version_8": `alter table feeds add column crawler boolean default 'f';
|
||||
`,
|
||||
"schema_version_9": `alter table sessions rename to user_sessions;`,
|
||||
}
|
||||
|
||||
var SqlMapChecksums = map[string]string{
|
||||
"schema_version_1": "00b2fa9e945565625c93ef9d4242a8b6583dc3cd7edf38d2fc95c0f3f7b926ae",
|
||||
"schema_version_10": "8faf15ddeff7c8cc305e66218face11ed92b97df2bdc2d0d7944d61441656795",
|
||||
"schema_version_11": "dc5bbc302e01e425b49c48ddcd8e29e3ab2bb8e73a6cd1858a6ba9fbec0b5243",
|
||||
"schema_version_12": "a95abab6cdf64811fc744abd37457e2928939d999c5ef00d2bdd9398e16f32fb",
|
||||
"schema_version_13": "9073fae1e796936f4a43a8120ebdb4218442fe7d346ace6387556a357c2d7edf",
|
||||
"schema_version_14": "4622e42c4a5a88b6fe1e61f3d367b295968f7260ab5b96481760775ba9f9e1fe",
|
||||
"schema_version_15": "13ff91462bdf4cda5a94a4c7a09f757761b0f2c32b4be713ba4786a4837750e4",
|
||||
"schema_version_16": "9d006faca62fd7ab787f64aef0e0a5933d142466ec4cab0e096bb920d2797e34",
|
||||
"schema_version_17": "b9f15d6217275fedcf6d948dd85ebe978b869bf37f42a86fd5b50a51919fa0e1",
|
||||
"schema_version_18": "c0ec24847612c7f2dc326cf735baffba79391a56aedd73292371a39f38724a71",
|
||||
"schema_version_19": "a83f77b41cc213d282805a5b518f15abbf96331599119f0ef4aca4be037add7b",
|
||||
"schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
|
||||
"schema_version_20": "5d414c0cfc0da2863c641079afa58b7ff42dccb0f0e01c822ad435c3e3aa9201",
|
||||
"schema_version_21": "77da01ee38918ff4fe33985fbb20ed3276a717a7584c2ca9ebcf4d4ab6cb6910",
|
||||
"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
|
||||
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
|
||||
"schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c",
|
||||
"schema_version_6": "9d05b4fb223f0e60efc716add5048b0ca9c37511cf2041721e20505d6d798ce4",
|
||||
"schema_version_7": "33f298c9aa30d6de3ca28e1270df51c2884d7596f1283a75716e2aeb634cd05c",
|
||||
"schema_version_8": "9922073fc4032d8922617ec6a6a07ae8d4817846c138760fb96cb5608ab83bfc",
|
||||
"schema_version_9": "de5ba954752fe808a993feef5bf0c6f808e0a4ced5379de8bec8342678150892",
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
create table schema_version (
|
||||
version text not null
|
||||
);
|
||||
|
||||
create table users (
|
||||
id serial not null,
|
||||
username text not null unique,
|
||||
password text,
|
||||
is_admin bool default 'f',
|
||||
language text default 'en_US',
|
||||
timezone text default 'UTC',
|
||||
theme text default 'default',
|
||||
last_login_at timestamp with time zone,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
create table sessions (
|
||||
id serial not null,
|
||||
user_id int not null,
|
||||
token text not null unique,
|
||||
created_at timestamp with time zone default now(),
|
||||
user_agent text,
|
||||
ip text,
|
||||
primary key (id),
|
||||
unique (user_id, token),
|
||||
foreign key (user_id) references users(id) on delete cascade
|
||||
);
|
||||
|
||||
create table categories (
|
||||
id serial not null,
|
||||
user_id int not null,
|
||||
title text not null,
|
||||
primary key (id),
|
||||
unique (user_id, title),
|
||||
foreign key (user_id) references users(id) on delete cascade
|
||||
);
|
||||
|
||||
create table feeds (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
category_id int not null,
|
||||
title text not null,
|
||||
feed_url text not null,
|
||||
site_url text not null,
|
||||
checked_at timestamp with time zone default now(),
|
||||
etag_header text default '',
|
||||
last_modified_header text default '',
|
||||
parsing_error_msg text default '',
|
||||
parsing_error_count int default 0,
|
||||
primary key (id),
|
||||
unique (user_id, feed_url),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (category_id) references categories(id) on delete cascade
|
||||
);
|
||||
|
||||
create type entry_status as enum('unread', 'read', 'removed');
|
||||
|
||||
create table entries (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
feed_id bigint not null,
|
||||
hash text not null,
|
||||
published_at timestamp with time zone not null,
|
||||
title text not null,
|
||||
url text not null,
|
||||
author text,
|
||||
content text,
|
||||
status entry_status default 'unread',
|
||||
primary key (id),
|
||||
unique (feed_id, hash),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (feed_id) references feeds(id) on delete cascade
|
||||
);
|
||||
|
||||
create index entries_feed_idx on entries using btree(feed_id);
|
||||
|
||||
create table enclosures (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
entry_id bigint not null,
|
||||
url text not null,
|
||||
size int default 0,
|
||||
mime_type text default '',
|
||||
primary key (id),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (entry_id) references entries(id) on delete cascade
|
||||
);
|
||||
|
||||
create table icons (
|
||||
id bigserial not null,
|
||||
hash text not null unique,
|
||||
mime_type text not null,
|
||||
content bytea not null,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
create table feed_icons (
|
||||
feed_id bigint not null,
|
||||
icon_id bigint not null,
|
||||
primary key(feed_id, icon_id),
|
||||
foreign key (feed_id) references feeds(id) on delete cascade,
|
||||
foreign key (icon_id) references icons(id) on delete cascade
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
drop table tokens;
|
||||
|
||||
create table sessions (
|
||||
id text not null,
|
||||
data jsonb not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
primary key(id)
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
alter table integrations add column wallabag_enabled bool default 'f';
|
||||
alter table integrations add column wallabag_url text default '';
|
||||
alter table integrations add column wallabag_client_id text default '';
|
||||
alter table integrations add column wallabag_client_secret text default '';
|
||||
alter table integrations add column wallabag_username text default '';
|
||||
alter table integrations add column wallabag_password text default '';
|
||||
@@ -1 +0,0 @@
|
||||
alter table entries add column starred bool default 'f';
|
||||
@@ -1,2 +0,0 @@
|
||||
create index entries_user_status_idx on entries(user_id, status);
|
||||
create index feeds_user_category_idx on feeds(user_id, category_id);
|
||||
@@ -1,3 +0,0 @@
|
||||
alter table integrations add column nunux_keeper_enabled bool default 'f';
|
||||
alter table integrations add column nunux_keeper_url text default '';
|
||||
alter table integrations add column nunux_keeper_api_key text default '';
|
||||
@@ -1 +0,0 @@
|
||||
alter table enclosures alter column size set data type bigint;
|
||||
@@ -1 +0,0 @@
|
||||
alter table entries add column comments_url text default '';
|
||||
@@ -1,3 +0,0 @@
|
||||
alter table integrations add column pocket_enabled bool default 'f';
|
||||
alter table integrations add column pocket_access_token text default '';
|
||||
alter table integrations add column pocket_consumer_key text default '';
|
||||
@@ -1 +0,0 @@
|
||||
alter table user_sessions alter column ip set data type inet using ip::inet;
|
||||
@@ -1,2 +0,0 @@
|
||||
alter table feeds add column username text default '';
|
||||
alter table feeds add column password text default '';
|
||||
@@ -1,3 +0,0 @@
|
||||
create extension if not exists hstore;
|
||||
alter table users add column extra hstore;
|
||||
create index users_extra_idx on users using gin(extra);
|
||||
@@ -1,3 +0,0 @@
|
||||
alter table entries add column document_vectors tsvector;
|
||||
update entries set document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
|
||||
create index document_vectors_idx on entries using gin(document_vectors);
|
||||
@@ -1 +0,0 @@
|
||||
alter table feeds add column user_agent text default '';
|
||||
@@ -1,6 +0,0 @@
|
||||
create table tokens (
|
||||
id text not null,
|
||||
value text not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
primary key(id, value)
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
create type entry_sorting_direction as enum('asc', 'desc');
|
||||
alter table users add column entry_direction entry_sorting_direction default 'asc';
|
||||
@@ -1,15 +0,0 @@
|
||||
create table integrations (
|
||||
user_id int not null,
|
||||
pinboard_enabled bool default 'f',
|
||||
pinboard_token text default '',
|
||||
pinboard_tags text default 'miniflux',
|
||||
pinboard_mark_as_unread bool default 'f',
|
||||
instapaper_enabled bool default 'f',
|
||||
instapaper_username text default '',
|
||||
instapaper_password text default '',
|
||||
fever_enabled bool default 'f',
|
||||
fever_username text default '',
|
||||
fever_password text default '',
|
||||
fever_token text default '',
|
||||
primary key(user_id)
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
alter table feeds add column scraper_rules text default '';
|
||||
@@ -1 +0,0 @@
|
||||
alter table feeds add column rewrite_rules text default '';
|
||||
@@ -1 +0,0 @@
|
||||
alter table feeds add column crawler boolean default 'f';
|
||||
@@ -1 +0,0 @@
|
||||
alter table sessions rename to user_sessions;
|
||||
+55
-39
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/integration"
|
||||
@@ -22,8 +21,8 @@ import (
|
||||
)
|
||||
|
||||
// Serve handles Fever API calls.
|
||||
func Serve(router *mux.Router, cfg *config.Config, store *storage.Storage) {
|
||||
handler := &handler{cfg, store}
|
||||
func Serve(router *mux.Router, store *storage.Storage) {
|
||||
handler := &handler{store}
|
||||
|
||||
sr := router.PathPrefix("/fever").Subrouter()
|
||||
sr.Use(newMiddleware(store).serve)
|
||||
@@ -31,7 +30,6 @@ func Serve(router *mux.Router, cfg *config.Config, store *storage.Storage) {
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
cfg *config.Config
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
@@ -82,7 +80,7 @@ is_spark equal to 1.
|
||||
*/
|
||||
func (h *handler) handleGroups(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching groups for userID=%d", userID)
|
||||
logger.Debug("[Fever] Fetching groups for user #%d", userID)
|
||||
|
||||
categories, err := h.store.Categories(userID)
|
||||
if err != nil {
|
||||
@@ -185,7 +183,7 @@ A PHP/HTML example:
|
||||
*/
|
||||
func (h *handler) handleFavicons(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching favicons for userID=%d", userID)
|
||||
logger.Debug("[Fever] Fetching favicons for user #%d", userID)
|
||||
|
||||
icons, err := h.store.Icons(userID)
|
||||
if err != nil {
|
||||
@@ -240,7 +238,6 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
var result itemsResponse
|
||||
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching items for userID=%d", userID)
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
@@ -248,27 +245,38 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
builder.WithOrder("id")
|
||||
builder.WithDirection(model.DefaultSortingDirection)
|
||||
|
||||
sinceID := request.QueryIntParam(r, "since_id", 0)
|
||||
if sinceID > 0 {
|
||||
builder.AfterEntryID(int64(sinceID))
|
||||
}
|
||||
|
||||
maxID := request.QueryIntParam(r, "max_id", 0)
|
||||
if maxID > 0 {
|
||||
builder.WithOffset(maxID)
|
||||
}
|
||||
|
||||
csvItemIDs := request.QueryStringParam(r, "with_ids", "")
|
||||
if csvItemIDs != "" {
|
||||
var itemIDs []int64
|
||||
|
||||
for _, strItemID := range strings.Split(csvItemIDs, ",") {
|
||||
strItemID = strings.TrimSpace(strItemID)
|
||||
itemID, _ := strconv.Atoi(strItemID)
|
||||
itemIDs = append(itemIDs, int64(itemID))
|
||||
switch {
|
||||
case request.HasQueryParam(r, "since_id"):
|
||||
sinceID := request.QueryInt64Param(r, "since_id", 0)
|
||||
if sinceID > 0 {
|
||||
logger.Debug("[Fever] Fetching items since #%d for user #%d", sinceID, userID)
|
||||
builder.AfterEntryID(sinceID)
|
||||
}
|
||||
case request.HasQueryParam(r, "max_id"):
|
||||
maxID := request.QueryInt64Param(r, "max_id", 0)
|
||||
if maxID == 0 {
|
||||
logger.Debug("[Fever] Fetching most recent items for user #%d", userID)
|
||||
builder.WithDirection("desc")
|
||||
} else if maxID > 0 {
|
||||
logger.Debug("[Fever] Fetching items before #%d for user #%d", maxID, userID)
|
||||
builder.BeforeEntryID(maxID)
|
||||
builder.WithDirection("desc")
|
||||
}
|
||||
case request.HasQueryParam(r, "with_ids"):
|
||||
csvItemIDs := request.QueryStringParam(r, "with_ids", "")
|
||||
if csvItemIDs != "" {
|
||||
var itemIDs []int64
|
||||
|
||||
builder.WithEntryIDs(itemIDs)
|
||||
for _, strItemID := range strings.Split(csvItemIDs, ",") {
|
||||
strItemID = strings.TrimSpace(strItemID)
|
||||
itemID, _ := strconv.ParseInt(strItemID, 10, 64)
|
||||
itemIDs = append(itemIDs, itemID)
|
||||
}
|
||||
|
||||
builder.WithEntryIDs(itemIDs)
|
||||
}
|
||||
default:
|
||||
logger.Debug("[Fever] Fetching oldest items for user #%d", userID)
|
||||
}
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
@@ -323,19 +331,19 @@ A request with the unread_item_ids argument will return one additional member:
|
||||
*/
|
||||
func (h *handler) handleUnreadItems(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching unread items for userID=%d", userID)
|
||||
logger.Debug("[Fever] Fetching unread items for user #%d", userID)
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
entries, err := builder.GetEntries()
|
||||
rawEntryIDs, err := builder.GetEntryIDs()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var itemIDs []string
|
||||
for _, entry := range entries {
|
||||
itemIDs = append(itemIDs, strconv.FormatInt(entry.ID, 10))
|
||||
for _, entryID := range rawEntryIDs {
|
||||
itemIDs = append(itemIDs, strconv.FormatInt(entryID, 10))
|
||||
}
|
||||
|
||||
var result unreadResponse
|
||||
@@ -354,7 +362,7 @@ with the remote Fever installation.
|
||||
*/
|
||||
func (h *handler) handleSavedItems(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching saved items for userID=%d", userID)
|
||||
logger.Debug("[Fever] Fetching saved items for user #%d", userID)
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStarred()
|
||||
@@ -382,7 +390,7 @@ func (h *handler) handleSavedItems(w http.ResponseWriter, r *http.Request) {
|
||||
*/
|
||||
func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID)
|
||||
logger.Debug("[Fever] Receiving mark=item call for user #%d", userID)
|
||||
|
||||
entryID := request.FormInt64Value(r, "id")
|
||||
if entryID <= 0 {
|
||||
@@ -400,18 +408,20 @@ func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
logger.Debug("[Fever] Marking entry #%d but not found, ignored", entryID)
|
||||
json.OK(w, r, newBaseResponse())
|
||||
return
|
||||
}
|
||||
|
||||
switch r.FormValue("as") {
|
||||
case "read":
|
||||
logger.Debug("[Fever] Mark entry #%d as read", entryID)
|
||||
logger.Debug("[Fever] Mark entry #%d as read for user #%d", entryID, userID)
|
||||
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
|
||||
case "unread":
|
||||
logger.Debug("[Fever] Mark entry #%d as unread", entryID)
|
||||
logger.Debug("[Fever] Mark entry #%d as unread for user #%d", entryID, userID)
|
||||
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
|
||||
case "saved", "unsaved":
|
||||
logger.Debug("[Fever] Mark entry #%d as saved/unsaved", entryID)
|
||||
case "saved":
|
||||
logger.Debug("[Fever] Mark entry #%d as saved for user #%d", entryID, userID)
|
||||
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -424,8 +434,14 @@ func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
go func() {
|
||||
integration.SendEntry(h.cfg, entry, settings)
|
||||
integration.SendEntry(entry, settings)
|
||||
}()
|
||||
case "unsaved":
|
||||
logger.Debug("[Fever] Mark entry #%d as unsaved for user #%d", entryID, userID)
|
||||
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
json.OK(w, r, newBaseResponse())
|
||||
@@ -442,7 +458,7 @@ func (h *handler) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.FormInt64Value(r, "id")
|
||||
before := time.Unix(request.FormInt64Value(r, "before"), 0)
|
||||
|
||||
logger.Debug("[Fever] mark=feed, userID=%d, feedID=%d, before=%v", userID, feedID, before)
|
||||
logger.Debug("[Fever] Mark feed #%d as read for user #%d before %v", feedID, userID, before)
|
||||
|
||||
if feedID <= 0 {
|
||||
return
|
||||
@@ -468,7 +484,7 @@ func (h *handler) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
|
||||
groupID := request.FormInt64Value(r, "id")
|
||||
before := time.Unix(request.FormInt64Value(r, "before"), 0)
|
||||
|
||||
logger.Debug("[Fever] mark=group, userID=%d, groupID=%d, before=%v", userID, groupID, before)
|
||||
logger.Debug("[Fever] Mark group #%d as read for user #%d before %v", groupID, userID, before)
|
||||
|
||||
if groupID < 0 {
|
||||
return
|
||||
|
||||
+4
-3
@@ -24,9 +24,10 @@ func newMiddleware(s *storage.Storage) *middleware {
|
||||
|
||||
func (m *middleware) serve(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP := request.ClientIP(r)
|
||||
apiKey := r.FormValue("api_key")
|
||||
if apiKey == "" {
|
||||
logger.Info("[Fever] No API key provided")
|
||||
logger.Info("[Fever] [ClientIP=%s] No API key provided", clientIP)
|
||||
json.OK(w, r, newAuthFailureResponse())
|
||||
return
|
||||
}
|
||||
@@ -39,12 +40,12 @@ func (m *middleware) serve(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Info("[Fever] No user found with this API key")
|
||||
logger.Info("[Fever] [ClientIP=%s] No user found with this API key", clientIP)
|
||||
json.OK(w, r, newAuthFailureResponse())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[Fever] User #%d is authenticated", user.ID)
|
||||
logger.Info("[Fever] [ClientIP=%s] User #%d is authenticated with user agent %q", clientIP, user.ID, r.UserAgent())
|
||||
m.store.SetLastLogin(user.ID)
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
-227
@@ -1,227 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/tdewolff/minify/v2"
|
||||
"github.com/tdewolff/minify/v2/css"
|
||||
"github.com/tdewolff/minify/v2/js"
|
||||
)
|
||||
|
||||
const tpl = `// Code generated by go generate; DO NOT EDIT.
|
||||
|
||||
package {{ .Package }} // import "miniflux.app/{{ .ImportPath }}"
|
||||
|
||||
var {{ .Map }} = map[string]string{
|
||||
{{ range $constant, $content := .Files }}` + "\t" + `"{{ $constant }}": ` + "`{{ $content }}`" + `,
|
||||
{{ end }}}
|
||||
|
||||
var {{ .Map }}Checksums = map[string]string{
|
||||
{{ range $constant, $content := .Checksums }}` + "\t" + `"{{ $constant }}": "{{ $content }}",
|
||||
{{ end }}}
|
||||
`
|
||||
|
||||
var bundleTpl = template.Must(template.New("").Parse(tpl))
|
||||
|
||||
type Bundle struct {
|
||||
Package string
|
||||
Map string
|
||||
ImportPath string
|
||||
Files map[string]string
|
||||
Checksums map[string]string
|
||||
}
|
||||
|
||||
func (b *Bundle) Write(filename string) {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
bundleTpl.Execute(f, b)
|
||||
}
|
||||
|
||||
func NewBundle(pkg, mapName, importPath string) *Bundle {
|
||||
return &Bundle{
|
||||
Package: pkg,
|
||||
Map: mapName,
|
||||
ImportPath: importPath,
|
||||
Files: make(map[string]string),
|
||||
Checksums: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(filename string) []byte {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func checksum(data []byte) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256(data))
|
||||
}
|
||||
|
||||
func basename(filename string) string {
|
||||
return path.Base(filename)
|
||||
}
|
||||
|
||||
func stripExtension(filename string) string {
|
||||
filename = strings.TrimSuffix(filename, path.Ext(filename))
|
||||
return strings.Replace(filename, " ", "_", -1)
|
||||
}
|
||||
|
||||
func glob(pattern string) []string {
|
||||
// There is no Glob function in path package, so we have to use filepath and replace in case of Windows
|
||||
files, _ := filepath.Glob(pattern)
|
||||
for i := range files {
|
||||
if strings.Contains(files[i], "\\") {
|
||||
files[i] = strings.Replace(files[i], "\\", "/", -1)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func concat(files []string) string {
|
||||
var b strings.Builder
|
||||
for _, file := range files {
|
||||
b.Write(readFile(file))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func generateJSBundle(bundleFile string, bundleFiles map[string][]string, prefixes, suffixes map[string]string) {
|
||||
bundle := NewBundle("static", "Javascripts", "ui/static")
|
||||
m := minify.New()
|
||||
m.AddFunc("text/javascript", js.Minify)
|
||||
|
||||
for name, srcFiles := range bundleFiles {
|
||||
var b strings.Builder
|
||||
|
||||
if prefix, found := prefixes[name]; found {
|
||||
b.WriteString(prefix)
|
||||
}
|
||||
|
||||
b.WriteString(concat(srcFiles))
|
||||
|
||||
if suffix, found := suffixes[name]; found {
|
||||
b.WriteString(suffix)
|
||||
}
|
||||
|
||||
minifiedData, err := m.String("text/javascript", b.String())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bundle.Files[name] = minifiedData
|
||||
bundle.Checksums[name] = checksum([]byte(minifiedData))
|
||||
}
|
||||
|
||||
bundle.Write(bundleFile)
|
||||
}
|
||||
|
||||
func generateCSSBundle(bundleFile string, themes map[string][]string) {
|
||||
bundle := NewBundle("static", "Stylesheets", "ui/static")
|
||||
m := minify.New()
|
||||
m.AddFunc("text/css", css.Minify)
|
||||
|
||||
for theme, srcFiles := range themes {
|
||||
data := concat(srcFiles)
|
||||
minifiedData, err := m.String("text/css", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bundle.Files[theme] = minifiedData
|
||||
bundle.Checksums[theme] = checksum([]byte(minifiedData))
|
||||
}
|
||||
|
||||
bundle.Write(bundleFile)
|
||||
}
|
||||
|
||||
func generateBinaryBundle(bundleFile string, srcFiles []string) {
|
||||
bundle := NewBundle("static", "Binaries", "ui/static")
|
||||
|
||||
for _, srcFile := range srcFiles {
|
||||
data := readFile(srcFile)
|
||||
filename := basename(srcFile)
|
||||
encodedData := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
bundle.Files[filename] = string(encodedData)
|
||||
bundle.Checksums[filename] = checksum(data)
|
||||
}
|
||||
|
||||
bundle.Write(bundleFile)
|
||||
}
|
||||
|
||||
func generateBundle(bundleFile, pkg, mapName string, srcFiles []string) {
|
||||
bundle := NewBundle(pkg, mapName, pkg)
|
||||
|
||||
for _, srcFile := range srcFiles {
|
||||
data := readFile(srcFile)
|
||||
filename := stripExtension(basename(srcFile))
|
||||
|
||||
bundle.Files[filename] = string(data)
|
||||
bundle.Checksums[filename] = checksum(data)
|
||||
}
|
||||
|
||||
bundle.Write(bundleFile)
|
||||
}
|
||||
|
||||
func main() {
|
||||
generateJSBundle("ui/static/js.go", map[string][]string{
|
||||
"app": []string{
|
||||
"ui/static/js/dom_helper.js",
|
||||
"ui/static/js/touch_handler.js",
|
||||
"ui/static/js/keyboard_handler.js",
|
||||
"ui/static/js/mouse_handler.js",
|
||||
"ui/static/js/form_handler.js",
|
||||
"ui/static/js/request_builder.js",
|
||||
"ui/static/js/unread_counter_handler.js",
|
||||
"ui/static/js/entry_handler.js",
|
||||
"ui/static/js/feed_handler.js",
|
||||
"ui/static/js/confirm_handler.js",
|
||||
"ui/static/js/menu_handler.js",
|
||||
"ui/static/js/modal_handler.js",
|
||||
"ui/static/js/nav_handler.js",
|
||||
"ui/static/js/link_state_handler.js",
|
||||
"ui/static/js/bootstrap.js",
|
||||
},
|
||||
"sw": []string{
|
||||
"ui/static/js/sw.js",
|
||||
},
|
||||
}, map[string]string{
|
||||
"app": "(function(){'use strict';",
|
||||
"sw": "'use strict';",
|
||||
}, map[string]string{
|
||||
"app": "})();",
|
||||
})
|
||||
|
||||
generateCSSBundle("ui/static/css.go", map[string][]string{
|
||||
"default": []string{"ui/static/css/common.css"},
|
||||
"black": []string{"ui/static/css/common.css", "ui/static/css/black.css"},
|
||||
"sansserif": []string{"ui/static/css/common.css", "ui/static/css/sansserif.css"},
|
||||
})
|
||||
|
||||
generateBinaryBundle("ui/static/bin.go", glob("ui/static/bin/*"))
|
||||
|
||||
generateBundle("database/sql.go", "database", "SqlMap", glob("database/sql/*.sql"))
|
||||
generateBundle("template/views.go", "template", "templateViewsMap", glob("template/html/*.html"))
|
||||
generateBundle("template/common.go", "template", "templateCommonMap", glob("template/html/common/*.html"))
|
||||
generateBundle("locale/translations.go", "locale", "translations", glob("locale/translations/*.json"))
|
||||
}
|
||||
@@ -1,19 +1,27 @@
|
||||
module miniflux.app
|
||||
|
||||
// +heroku goVersion go1.11
|
||||
// +heroku goVersion go1.16
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.5.0
|
||||
github.com/golang/protobuf v1.1.0 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/mux v1.6.2
|
||||
github.com/lib/pq v1.0.0
|
||||
github.com/tdewolff/minify/v2 v2.3.8 // indirect
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
|
||||
golang.org/x/net v0.0.0-20181207154023-610586996380
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
|
||||
golang.org/x/sys v0.0.0-20181208175041-ad97f365e150 // indirect
|
||||
golang.org/x/text v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.1.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.6.1
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 // indirect
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/lib/pq v1.10.1
|
||||
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/prometheus/client_golang v1.10.0
|
||||
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d
|
||||
github.com/stretchr/testify v1.6.1 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.9.16
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.0 // indirect
|
||||
mvdan.cc/xurls/v2 v2.2.0
|
||||
)
|
||||
|
||||
go 1.16
|
||||
|
||||
@@ -1,39 +1,570 @@
|
||||
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
|
||||
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.6.1 h1:FgjbQZKl5HTmcn4sKBgvx8vv63nhyhIpv7lJpFGCWpk=
|
||||
github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-delve/delve v1.5.0/go.mod h1:c6b3a1Gry6x8a4LGCe/CWzrocrfaHvkUxCj3k4bvSUQ=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
|
||||
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 h1:5vu7C+63KTbsSNnLhrgB98Sqy8MNVSW8FdhkcWA/3Rk=
|
||||
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686/go.mod h1:sam69Hju0uq+5uvLJUMDlsKlQ21Vrs1Kd/1YFPNYdOU=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
|
||||
github.com/google/go-dap v0.3.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
|
||||
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2 h1:9kYRAm87/M5VL+HWegDGIorBWDiErrZrksLKTJBF2IQ=
|
||||
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2/go.mod h1:8mKJVJkyMI20ifXXO0zlV3sh3FrtjvltAeBqz38clBE=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
|
||||
github.com/peterh/liner v1.2.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.10.0 h1:/o0BDeWzLWXNZ+4q5gXltUvaMpJqckTa+jTNoB+z4cg=
|
||||
github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.18.0 h1:WCVKW7aL6LEe1uryfI9dnEc2ZqNB1Fn0ok930v0iL1Y=
|
||||
github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d h1:4Jn2kzSKcpxxwpJdyWxfNWKCYe4Uki2VibkExYmBLiA=
|
||||
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d/go.mod h1:3vfmZI6aJd5Rb9W2TQ0Nmupl+qem21R05+hmCscI0Bk=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/tdewolff/minify/v2 v2.3.8 h1:Eyv23Tu+Rb5Q2vyxmvzUgtHetgneqAsaGv3950s1EeA=
|
||||
github.com/tdewolff/minify/v2 v2.3.8/go.mod h1:DD1stRlSx6JsHfl1+E/HVMQeXiec9rD1UQ0epklIZLc=
|
||||
github.com/tdewolff/parse/v2 v2.3.5 h1:/uS8JfhwVJsNkEh769GM5ENv6L9LOh2Z9uW3tCdlhs0=
|
||||
github.com/tdewolff/parse/v2 v2.3.5/go.mod h1:HansaqmN4I/U7L6/tUp0NcwT2tFO0F4EAWYGSDzkYNk=
|
||||
github.com/tdewolff/test v1.0.0/go.mod h1:DiQUlutnqlEvdvhSn2LPGy4TFwRauAaYDsL+683RNX4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tdewolff/minify/v2 v2.9.16 h1:2Pv8pFRX/ZfjTRYX2xzcuNrkEJqU5TfriNJJYOeN3rI=
|
||||
github.com/tdewolff/minify/v2 v2.9.16/go.mod h1:cjMkr4ZgFjqxXAQ1kR9Fm4l1046mmONd2g6yMzGuN/w=
|
||||
github.com/tdewolff/parse/v2 v2.5.14 h1:ftdD54vkOeLZ7VkEZxp+wZrYZyyPi43GGon5GwBTRUI=
|
||||
github.com/tdewolff/parse/v2 v2.5.14/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
|
||||
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
|
||||
go.starlark.net v0.0.0-20201006213952-227f4aabceb5/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
|
||||
golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181207154023-610586996380 h1:zPQexyRtNYBc7bcHmehl1dH6TB3qn8zytv8cBGLDNY0=
|
||||
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d h1:dOiJ2n2cMwGLce/74I/QHMbnpk5GfY7InR8rczoMqRM=
|
||||
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181208175041-ad97f365e150 h1:a+Y1JYYfPXfb6xhWlOV++uijPyhhJy5Y/ROUAOZSre4=
|
||||
golang.org/x/sys v0.0.0-20181208175041-ad97f365e150/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.5.0 h1:OZ4sdq+Y+SHfYB7vfthi1Ei8b0vkP8ZPQgUfUwdUSqo=
|
||||
gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
mvdan.cc/xurls/v2 v2.2.0 h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A=
|
||||
mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
|
||||
+168
-63
@@ -11,80 +11,147 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/errors"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/timer"
|
||||
"miniflux.app/version"
|
||||
url_helper "miniflux.app/url"
|
||||
)
|
||||
|
||||
const (
|
||||
// 20 seconds max.
|
||||
requestTimeout = 20
|
||||
|
||||
// 15MB max.
|
||||
maxBodySize = 1024 * 1024 * 15
|
||||
defaultHTTPClientTimeout = 20
|
||||
defaultHTTPClientMaxBodySize = 15 * 1024 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultUserAgent sets the User-Agent header used for any requests by miniflux.
|
||||
DefaultUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)"
|
||||
|
||||
errInvalidCertificate = "Invalid SSL certificate (original error: %q)"
|
||||
errTemporaryNetworkOperation = "This website is temporarily unreachable (original error: %q)"
|
||||
errPermanentNetworkOperation = "This website is permanently unreachable (original error: %q)"
|
||||
errRequestTimeout = "Website unreachable, the request timed out after %d seconds"
|
||||
)
|
||||
|
||||
// Client is a HTTP Client :)
|
||||
// Client builds and executes HTTP requests.
|
||||
type Client struct {
|
||||
url string
|
||||
etagHeader string
|
||||
lastModifiedHeader string
|
||||
authorizationHeader string
|
||||
username string
|
||||
password string
|
||||
userAgent string
|
||||
Insecure bool
|
||||
inputURL string
|
||||
|
||||
requestURL string
|
||||
requestEtagHeader string
|
||||
requestLastModifiedHeader string
|
||||
requestAuthorizationHeader string
|
||||
requestUsername string
|
||||
requestPassword string
|
||||
requestUserAgent string
|
||||
requestCookie string
|
||||
|
||||
useProxy bool
|
||||
doNotFollowRedirects bool
|
||||
|
||||
ClientTimeout int
|
||||
ClientMaxBodySize int64
|
||||
ClientProxyURL string
|
||||
AllowSelfSignedCertificates bool
|
||||
}
|
||||
|
||||
// New initializes a new HTTP client.
|
||||
func New(url string) *Client {
|
||||
return &Client{
|
||||
inputURL: url,
|
||||
ClientTimeout: defaultHTTPClientTimeout,
|
||||
ClientMaxBodySize: defaultHTTPClientMaxBodySize,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientWithConfig initializes a new HTTP client with application config options.
|
||||
func NewClientWithConfig(url string, opts *config.Options) *Client {
|
||||
return &Client{
|
||||
inputURL: url,
|
||||
requestUserAgent: opts.HTTPClientUserAgent(),
|
||||
ClientTimeout: opts.HTTPClientTimeout(),
|
||||
ClientMaxBodySize: opts.HTTPClientMaxBodySize(),
|
||||
ClientProxyURL: opts.HTTPClientProxy(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) String() string {
|
||||
etagHeader := c.requestEtagHeader
|
||||
if c.requestEtagHeader == "" {
|
||||
etagHeader = "None"
|
||||
}
|
||||
|
||||
lastModifiedHeader := c.requestLastModifiedHeader
|
||||
if c.requestLastModifiedHeader == "" {
|
||||
lastModifiedHeader = "None"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`InputURL=%q RequestURL=%q ETag=%s LastMod=%s Auth=%v UserAgent=%q Verify=%v`,
|
||||
c.inputURL,
|
||||
c.requestURL,
|
||||
etagHeader,
|
||||
lastModifiedHeader,
|
||||
c.requestAuthorizationHeader != "" || (c.requestUsername != "" && c.requestPassword != ""),
|
||||
c.requestUserAgent,
|
||||
!c.AllowSelfSignedCertificates,
|
||||
)
|
||||
}
|
||||
|
||||
// WithCredentials defines the username/password for HTTP Basic authentication.
|
||||
func (c *Client) WithCredentials(username, password string) *Client {
|
||||
if username != "" && password != "" {
|
||||
c.username = username
|
||||
c.password = password
|
||||
c.requestUsername = username
|
||||
c.requestPassword = password
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// WithAuthorization defines authorization header value.
|
||||
// WithAuthorization defines the authorization HTTP header value.
|
||||
func (c *Client) WithAuthorization(authorization string) *Client {
|
||||
c.authorizationHeader = authorization
|
||||
c.requestAuthorizationHeader = authorization
|
||||
return c
|
||||
}
|
||||
|
||||
// WithCacheHeaders defines caching headers.
|
||||
func (c *Client) WithCacheHeaders(etagHeader, lastModifiedHeader string) *Client {
|
||||
c.etagHeader = etagHeader
|
||||
c.lastModifiedHeader = lastModifiedHeader
|
||||
c.requestEtagHeader = etagHeader
|
||||
c.requestLastModifiedHeader = lastModifiedHeader
|
||||
return c
|
||||
}
|
||||
|
||||
// WithUserAgent defines the User-Agent header to use for outgoing requests.
|
||||
// WithProxy enables proxy for the current HTTP request.
|
||||
func (c *Client) WithProxy() *Client {
|
||||
c.useProxy = true
|
||||
return c
|
||||
}
|
||||
|
||||
// WithoutRedirects disables HTTP redirects.
|
||||
func (c *Client) WithoutRedirects() *Client {
|
||||
c.doNotFollowRedirects = true
|
||||
return c
|
||||
}
|
||||
|
||||
// WithUserAgent defines the User-Agent header to use for HTTP requests.
|
||||
func (c *Client) WithUserAgent(userAgent string) *Client {
|
||||
if userAgent != "" {
|
||||
c.userAgent = userAgent
|
||||
c.requestUserAgent = userAgent
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Get execute a GET HTTP request.
|
||||
// WithCookie defines the Cookies to use for HTTP requests.
|
||||
func (c *Client) WithCookie(cookie string) *Client {
|
||||
if cookie != "" {
|
||||
c.requestCookie = cookie
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Get performs a GET HTTP request.
|
||||
func (c *Client) Get() (*Response, error) {
|
||||
request, err := c.buildRequest(http.MethodGet, nil)
|
||||
if err != nil {
|
||||
@@ -94,7 +161,7 @@ func (c *Client) Get() (*Response, error) {
|
||||
return c.executeRequest(request)
|
||||
}
|
||||
|
||||
// PostForm execute a POST HTTP request with form values.
|
||||
// PostForm performs a POST HTTP request with form encoded values.
|
||||
func (c *Client) PostForm(values url.Values) (*Response, error) {
|
||||
request, err := c.buildRequest(http.MethodPost, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
@@ -105,7 +172,7 @@ func (c *Client) PostForm(values url.Values) (*Response, error) {
|
||||
return c.executeRequest(request)
|
||||
}
|
||||
|
||||
// PostJSON execute a POST HTTP request with JSON payload.
|
||||
// PostJSON performs a POST HTTP request with a JSON payload.
|
||||
func (c *Client) PostJSON(data interface{}) (*Response, error) {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
@@ -122,7 +189,12 @@ func (c *Client) PostJSON(data interface{}) (*Response, error) {
|
||||
}
|
||||
|
||||
func (c *Client) executeRequest(request *http.Request) (*Response, error) {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient] url=%s", c.url))
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient] inputURL=%s", c.inputURL))
|
||||
|
||||
logger.Debug("[HttpClient:Before] Method=%s %s",
|
||||
request.Method,
|
||||
c.String(),
|
||||
)
|
||||
|
||||
client := c.buildClient()
|
||||
resp, err := client.Do(request)
|
||||
@@ -144,7 +216,7 @@ func (c *Client) executeRequest(request *http.Request) (*Response, error) {
|
||||
case net.Error:
|
||||
nerr := uerr.Err.(net.Error)
|
||||
if nerr.Timeout() {
|
||||
err = errors.NewLocalizedError(errRequestTimeout, requestTimeout)
|
||||
err = errors.NewLocalizedError(errRequestTimeout, c.ClientTimeout)
|
||||
} else if nerr.Temporary() {
|
||||
err = errors.NewLocalizedError(errTemporaryNetworkOperation, nerr)
|
||||
}
|
||||
@@ -154,11 +226,11 @@ func (c *Client) executeRequest(request *http.Request) (*Response, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.ContentLength > maxBodySize {
|
||||
if resp.ContentLength > c.ClientMaxBodySize {
|
||||
return nil, fmt.Errorf("client: response too large (%d bytes)", resp.ContentLength)
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(resp.Body)
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("client: error while reading body %v", err)
|
||||
}
|
||||
@@ -169,21 +241,15 @@ func (c *Client) executeRequest(request *http.Request) (*Response, error) {
|
||||
EffectiveURL: resp.Request.URL.String(),
|
||||
LastModified: resp.Header.Get("Last-Modified"),
|
||||
ETag: resp.Header.Get("ETag"),
|
||||
Expires: resp.Header.Get("Expires"),
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
ContentLength: resp.ContentLength,
|
||||
}
|
||||
|
||||
logger.Debug("[HttpClient:%s] URL=%s, EffectiveURL=%s, Code=%d, Length=%d, Type=%s, ETag=%s, LastMod=%s, Expires=%s, Auth=%v",
|
||||
logger.Debug("[HttpClient:After] Method=%s %s; Response => %s",
|
||||
request.Method,
|
||||
c.url,
|
||||
response.EffectiveURL,
|
||||
response.StatusCode,
|
||||
resp.ContentLength,
|
||||
response.ContentType,
|
||||
response.ETag,
|
||||
response.LastModified,
|
||||
resp.Header.Get("Expires"),
|
||||
c.username != "",
|
||||
c.String(),
|
||||
response,
|
||||
)
|
||||
|
||||
// Ignore caching headers for feeds that do not want any cache.
|
||||
@@ -197,53 +263,92 @@ func (c *Client) executeRequest(request *http.Request) (*Response, error) {
|
||||
}
|
||||
|
||||
func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, error) {
|
||||
request, err := http.NewRequest(method, c.url, body)
|
||||
c.requestURL = url_helper.RequestURI(c.inputURL)
|
||||
request, err := http.NewRequest(method, c.requestURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header = c.buildHeaders()
|
||||
|
||||
if c.username != "" && c.password != "" {
|
||||
request.SetBasicAuth(c.username, c.password)
|
||||
if c.requestUsername != "" && c.requestPassword != "" {
|
||||
request.SetBasicAuth(c.requestUsername, c.requestPassword)
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (c *Client) buildClient() http.Client {
|
||||
client := http.Client{Timeout: time.Duration(requestTimeout * time.Second)}
|
||||
if c.Insecure {
|
||||
client.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(c.ClientTimeout) * time.Second,
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
// Default is 30s.
|
||||
Timeout: 10 * time.Second,
|
||||
|
||||
// Default is 30s.
|
||||
KeepAlive: 15 * time.Second,
|
||||
}).DialContext,
|
||||
|
||||
// Default is 100.
|
||||
MaxIdleConns: 50,
|
||||
|
||||
// Default is 90s.
|
||||
IdleConnTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
if c.AllowSelfSignedCertificates {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
if c.doNotFollowRedirects {
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
}
|
||||
|
||||
if c.useProxy && c.ClientProxyURL != "" {
|
||||
proxyURL, err := url.Parse(c.ClientProxyURL)
|
||||
if err != nil {
|
||||
logger.Error("[HttpClient] Proxy URL error: %v", err)
|
||||
} else {
|
||||
logger.Debug("[HttpClient] Use proxy: %s", proxyURL)
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
client.Transport = transport
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) buildHeaders() http.Header {
|
||||
headers := make(http.Header)
|
||||
headers.Add("User-Agent", c.userAgent)
|
||||
headers.Add("Accept", "*/*")
|
||||
|
||||
if c.etagHeader != "" {
|
||||
headers.Add("If-None-Match", c.etagHeader)
|
||||
if c.requestUserAgent != "" {
|
||||
headers.Add("User-Agent", c.requestUserAgent)
|
||||
}
|
||||
|
||||
if c.lastModifiedHeader != "" {
|
||||
headers.Add("If-Modified-Since", c.lastModifiedHeader)
|
||||
if c.requestEtagHeader != "" {
|
||||
headers.Add("If-None-Match", c.requestEtagHeader)
|
||||
}
|
||||
|
||||
if c.authorizationHeader != "" {
|
||||
headers.Add("Authorization", c.authorizationHeader)
|
||||
if c.requestLastModifiedHeader != "" {
|
||||
headers.Add("If-Modified-Since", c.requestLastModifiedHeader)
|
||||
}
|
||||
|
||||
if c.requestAuthorizationHeader != "" {
|
||||
headers.Add("Authorization", c.requestAuthorizationHeader)
|
||||
}
|
||||
|
||||
if c.requestCookie != "" {
|
||||
headers.Add("Cookie", c.requestCookie)
|
||||
}
|
||||
|
||||
headers.Add("Connection", "close")
|
||||
return headers
|
||||
}
|
||||
|
||||
// New returns a new HTTP client.
|
||||
func New(url string) *Client {
|
||||
return &Client{url: url, userAgent: DefaultUserAgent, Insecure: false}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2020 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package client // import "miniflux.app/http/client"
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientWithDelay(t *testing.T) {
|
||||
clt := New("http://httpbin.org/delay/5")
|
||||
clt.ClientTimeout = 1
|
||||
_, err := clt.Get()
|
||||
if err == nil {
|
||||
t.Fatal(`The client should stops after 1 second`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientWithError(t *testing.T) {
|
||||
clt := New("http://httpbin.org/status/502")
|
||||
clt.ClientTimeout = 1
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if response.StatusCode != 502 {
|
||||
t.Fatalf(`Unexpected response status code: %d`, response.StatusCode)
|
||||
}
|
||||
|
||||
if !response.HasServerFailure() {
|
||||
t.Fatal(`A 500 error is considered as server failure`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientWithResponseTooLarge(t *testing.T) {
|
||||
clt := New("http://httpbin.org/bytes/100")
|
||||
clt.ClientMaxBodySize = 10
|
||||
_, err := clt.Get()
|
||||
if err == nil {
|
||||
t.Fatal(`The client should fails when reading a response too large`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientWithBasicAuth(t *testing.T) {
|
||||
clt := New("http://httpbin.org/basic-auth/testuser/testpassword")
|
||||
clt.WithCredentials("testuser", "testpassword")
|
||||
_, err := clt.Get()
|
||||
if err != nil {
|
||||
t.Fatalf(`The client should be authenticated successfully: %v`, err)
|
||||
}
|
||||
}
|
||||
+36
-34
@@ -6,9 +6,8 @@ package client // import "miniflux.app/http/client"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -25,10 +24,24 @@ type Response struct {
|
||||
EffectiveURL string
|
||||
LastModified string
|
||||
ETag string
|
||||
Expires string
|
||||
ContentType string
|
||||
ContentLength int64
|
||||
}
|
||||
|
||||
func (r *Response) String() string {
|
||||
return fmt.Sprintf(
|
||||
`StatusCode=%d EffectiveURL=%q LastModified=%q ETag=%s Expires=%s ContentType=%q ContentLength=%d`,
|
||||
r.StatusCode,
|
||||
r.EffectiveURL,
|
||||
r.LastModified,
|
||||
r.ETag,
|
||||
r.Expires,
|
||||
r.ContentType,
|
||||
r.ContentLength,
|
||||
)
|
||||
}
|
||||
|
||||
// IsNotFound returns true if the resource doesn't exists anymore.
|
||||
func (r *Response) IsNotFound() bool {
|
||||
return r.StatusCode == 404 || r.StatusCode == 410
|
||||
@@ -73,37 +86,26 @@ func (r *Response) IsModified(etag, lastModified string) bool {
|
||||
// - Feeds with encoding specified only in XML document and not in HTTP header
|
||||
// - Feeds with wrong encoding defined and already in UTF-8
|
||||
func (r *Response) EnsureUnicodeBody() (err error) {
|
||||
if r.ContentType != "" {
|
||||
mediaType, _, mediaErr := mime.ParseMediaType(r.ContentType)
|
||||
if mediaErr != nil {
|
||||
return mediaErr
|
||||
buffer, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Body = bytes.NewReader(buffer)
|
||||
if utf8.Valid(buffer) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(r.ContentType, "xml") {
|
||||
// We ignore documents with encoding specified in XML prolog.
|
||||
// This is going to be handled by the XML parser.
|
||||
length := 1024
|
||||
if len(buffer) < 1024 {
|
||||
length = len(buffer)
|
||||
}
|
||||
|
||||
// JSON feeds are always in UTF-8.
|
||||
if strings.Contains(mediaType, "json") {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(mediaType, "xml") {
|
||||
buffer, _ := ioutil.ReadAll(r.Body)
|
||||
r.Body = bytes.NewReader(buffer)
|
||||
|
||||
// We ignore documents with encoding specified in XML prolog.
|
||||
// This is going to be handled by the XML parser.
|
||||
length := 1024
|
||||
if len(buffer) < 1024 {
|
||||
length = len(buffer)
|
||||
}
|
||||
|
||||
if xmlEncodingRegex.Match(buffer[0:length]) {
|
||||
return
|
||||
}
|
||||
|
||||
// If no encoding is specified in the XML prolog and
|
||||
// the document is valid UTF-8, nothing needs to be done.
|
||||
if utf8.Valid(buffer) {
|
||||
return
|
||||
}
|
||||
if xmlEncodingRegex.Match(buffer[0:length]) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +113,8 @@ func (r *Response) EnsureUnicodeBody() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// String returns the response body as string.
|
||||
func (r *Response) String() string {
|
||||
bytes, _ := ioutil.ReadAll(r.Body)
|
||||
// BodyAsString returns the response body as string.
|
||||
func (r *Response) BodyAsString() string {
|
||||
bytes, _ := io.ReadAll(r.Body)
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ package client // import "miniflux.app/http/client"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
@@ -95,8 +95,8 @@ func TestToString(t *testing.T) {
|
||||
input := `test`
|
||||
r := &Response{Body: strings.NewReader(input)}
|
||||
|
||||
if r.String() != input {
|
||||
t.Error(`Unexpected ouput`)
|
||||
if r.BodyAsString() != input {
|
||||
t.Error(`Unexpected output`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ func TestEnsureUnicodeWithHTMLDocuments(t *testing.T) {
|
||||
{"urdu.xml", "text/xml; charset=utf-8", true},
|
||||
{"content-type-only-win-8859-1.xml", "application/xml; charset=ISO-8859-1", true},
|
||||
{"rdf_utf8.xml", "application/rss+xml; charset=utf-8", true},
|
||||
{"rdf_utf8.xml", "application/rss+xml; charset: utf-8", true}, // Invalid Content-Type
|
||||
{"charset-content-type-xml-iso88591.xml", "application/rss+xml; charset=ISO-8859-1", false},
|
||||
{"windows_1251.xml", "text/xml", false},
|
||||
{"smallfile.xml", "text/xml; charset=utf-8", true},
|
||||
@@ -128,7 +129,7 @@ func TestEnsureUnicodeWithHTMLDocuments(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range unicodeTestCases {
|
||||
content, err := ioutil.ReadFile("testdata/" + tc.filename)
|
||||
content, err := os.ReadFile("testdata/" + tc.filename)
|
||||
if err != nil {
|
||||
t.Fatalf(`Unable to read file %q: %v`, tc.filename, err)
|
||||
}
|
||||
@@ -136,10 +137,10 @@ func TestEnsureUnicodeWithHTMLDocuments(t *testing.T) {
|
||||
r := &Response{Body: bytes.NewReader(content), ContentType: tc.contentType}
|
||||
parseErr := r.EnsureUnicodeBody()
|
||||
if parseErr != nil {
|
||||
t.Fatalf(`Unicode conversion error for %q - %q: %v`, tc.filename, tc.contentType, err)
|
||||
t.Fatalf(`Unicode conversion error for %q - %q: %v`, tc.filename, tc.contentType, parseErr)
|
||||
}
|
||||
|
||||
isUnicode := utf8.ValidString(r.String())
|
||||
isUnicode := utf8.ValidString(r.BodyAsString())
|
||||
if isUnicode != tc.convertedToUnicode {
|
||||
t.Errorf(`Unicode conversion %q - %q, got: %v, expected: %v`,
|
||||
tc.filename, tc.contentType, isUnicode, tc.convertedToUnicode)
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
// Cookie names.
|
||||
const (
|
||||
CookieSessionID = "sessionID"
|
||||
CookieUserSessionID = "userSessionID"
|
||||
CookieAppSessionID = "MinifluxAppSessionID"
|
||||
CookieUserSessionID = "MinifluxUserSessionID"
|
||||
|
||||
// Cookie duration in days.
|
||||
cookieDuration = 30
|
||||
|
||||
@@ -10,6 +10,14 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func dropIPv6zone(address string) string {
|
||||
i := strings.IndexByte(address, '%')
|
||||
if i != -1 {
|
||||
address = address[:i]
|
||||
}
|
||||
return address
|
||||
}
|
||||
|
||||
// FindClientIP returns client real IP address.
|
||||
func FindClientIP(r *http.Request) string {
|
||||
headers := []string{"X-Forwarded-For", "X-Real-Ip"}
|
||||
@@ -19,6 +27,7 @@ func FindClientIP(r *http.Request) string {
|
||||
if value != "" {
|
||||
addresses := strings.Split(value, ",")
|
||||
address := strings.TrimSpace(addresses[0])
|
||||
address = dropIPv6zone(address)
|
||||
|
||||
if net.ParseIP(address) != nil {
|
||||
return address
|
||||
@@ -27,12 +36,11 @@ func FindClientIP(r *http.Request) string {
|
||||
}
|
||||
|
||||
// Fallback to TCP/IP source IP address.
|
||||
var remoteIP string
|
||||
if strings.ContainsRune(r.RemoteAddr, ':') {
|
||||
remoteIP, _, _ = net.SplitHostPort(r.RemoteAddr)
|
||||
} else {
|
||||
remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteIP = r.RemoteAddr
|
||||
}
|
||||
remoteIP = dropIPv6zone(remoteIP)
|
||||
|
||||
// When listening on a Unix socket, RemoteAddr is empty.
|
||||
if remoteIP == "" {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user