Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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 }}
|
||||
+3
-1
@@ -1,2 +1,4 @@
|
||||
miniflux-*
|
||||
miniflux
|
||||
miniflux
|
||||
*.rpm
|
||||
*.deb
|
||||
|
||||
@@ -1,3 +1,95 @@
|
||||
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)
|
||||
-----------------------------
|
||||
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
ARG BASE_IMAGE_ARCH="amd64"
|
||||
ARG ALPINE_LINUX_VERSION="3.12"
|
||||
|
||||
FROM golang:1-alpine${ALPINE_LINUX_VERSION} as build
|
||||
ARG APP_VERSION
|
||||
ARG APP_ARCH="amd64"
|
||||
WORKDIR /go/src/app
|
||||
RUN apk add --no-cache --update build-base git
|
||||
COPY . .
|
||||
RUN make linux-${APP_ARCH} VERSION=${APP_VERSION}
|
||||
RUN cp /go/src/app/miniflux-linux-${APP_ARCH} /go/src/app/miniflux
|
||||
|
||||
FROM ${BASE_IMAGE_ARCH}/alpine:${ALPINE_LINUX_VERSION}
|
||||
EXPOSE 8080
|
||||
ENV LISTEN_ADDR 0.0.0.0:8080
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
COPY --from=build /go/src/app/miniflux /usr/bin/miniflux
|
||||
USER nobody
|
||||
CMD ["/usr/bin/miniflux"]
|
||||
@@ -1,18 +1,19 @@
|
||||
APP := miniflux
|
||||
APP := miniflux
|
||||
DOCKER_IMAGE := miniflux/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
|
||||
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 PGPASSWORD := postgres
|
||||
export GO111MODULE=on
|
||||
|
||||
.PHONY: generate \
|
||||
miniflux \
|
||||
linux-amd64 \
|
||||
linux-armv8 \
|
||||
linux-arm64 \
|
||||
linux-armv7 \
|
||||
linux-armv6 \
|
||||
linux-armv5 \
|
||||
@@ -35,10 +36,12 @@ export GO111MODULE=on
|
||||
clean-integration-test \
|
||||
docker-image \
|
||||
docker-images \
|
||||
docker-manifest
|
||||
rpm \
|
||||
debian \
|
||||
debian-packages
|
||||
|
||||
generate:
|
||||
@ go generate -mod=vendor
|
||||
@ go generate
|
||||
|
||||
miniflux: generate
|
||||
@ go build -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
@@ -46,8 +49,8 @@ miniflux: generate
|
||||
linux-amd64: generate
|
||||
@ GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-amd64 main.go
|
||||
|
||||
linux-armv8: generate
|
||||
@ GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv8 main.go
|
||||
linux-arm64: generate
|
||||
@ GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-arm64 main.go
|
||||
|
||||
linux-armv7: generate
|
||||
@ GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv7 main.go
|
||||
@@ -70,7 +73,7 @@ openbsd-amd64: generate
|
||||
windows-amd64: generate
|
||||
@ 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 freebsd-amd64 openbsd-amd64 windows-amd64
|
||||
|
||||
# NOTE: unsupported targets
|
||||
netbsd-amd64: generate
|
||||
@@ -92,10 +95,10 @@ windows-x86: generate
|
||||
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-x86 main.go
|
||||
|
||||
run: generate
|
||||
@ go run main.go -debug
|
||||
@ 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 -cover -race -count=1 ./...
|
||||
@@ -120,52 +123,34 @@ clean-integration-test:
|
||||
@ psql -U postgres -c 'drop database if exists miniflux_test;'
|
||||
|
||||
docker-image:
|
||||
docker build -t $(DOCKER_IMAGE):$(VERSION) \
|
||||
--build-arg APP_VERSION=$(VERSION) \
|
||||
--build-arg APP_ARCH=amd64 \
|
||||
--build-arg BASE_IMAGE_ARCH=amd64 .
|
||||
docker build -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/Dockerfile .
|
||||
|
||||
docker-images:
|
||||
docker build -t $(DOCKER_IMAGE):amd64-$(VERSION) \
|
||||
--build-arg APP_VERSION=$(VERSION) \
|
||||
--build-arg APP_ARCH=amd64 \
|
||||
--build-arg BASE_IMAGE_ARCH=amd64 .
|
||||
docker tag $(DOCKER_IMAGE):amd64-$(VERSION) $(DOCKER_IMAGE):amd64-latest
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 \
|
||||
--file packaging/docker/Dockerfile \
|
||||
--tag $(DOCKER_IMAGE):$(VERSION) \
|
||||
--push .
|
||||
|
||||
docker build -t $(DOCKER_IMAGE):arm32v6-$(VERSION) \
|
||||
--build-arg APP_VERSION=$(VERSION) \
|
||||
--build-arg APP_ARCH=armv6 \
|
||||
--build-arg BASE_IMAGE_ARCH=arm32v6 .
|
||||
docker tag $(DOCKER_IMAGE):arm32v6-$(VERSION) $(DOCKER_IMAGE):arm32v6-latest
|
||||
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
|
||||
|
||||
docker build -t $(DOCKER_IMAGE):arm32v7-$(VERSION) \
|
||||
--build-arg APP_VERSION=$(VERSION) \
|
||||
--build-arg APP_ARCH=armv7 \
|
||||
--build-arg BASE_IMAGE_ARCH=arm32v7 .
|
||||
docker tag $(DOCKER_IMAGE):arm32v7-$(VERSION) $(DOCKER_IMAGE):arm32v7-latest
|
||||
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
|
||||
|
||||
docker build -t $(DOCKER_IMAGE):arm64v8-$(VERSION) \
|
||||
--build-arg APP_VERSION=$(VERSION) \
|
||||
--build-arg APP_ARCH=armv8 \
|
||||
--build-arg BASE_IMAGE_ARCH=arm64v8 .
|
||||
docker tag $(DOCKER_IMAGE):arm64v8-$(VERSION) $(DOCKER_IMAGE):arm64v8-latest
|
||||
|
||||
docker-manifest:
|
||||
for version in $(VERSION) latest; do \
|
||||
docker push $(DOCKER_IMAGE):amd64-$${version} && \
|
||||
docker push $(DOCKER_IMAGE):arm32v6-$${version} && \
|
||||
docker push $(DOCKER_IMAGE):arm32v7-$${version} && \
|
||||
docker push $(DOCKER_IMAGE):arm64v8-$${version} && \
|
||||
docker manifest create --amend $(DOCKER_IMAGE):$${version} \
|
||||
$(DOCKER_IMAGE):amd64-$${version} \
|
||||
$(DOCKER_IMAGE):arm32v6-$${version} \
|
||||
$(DOCKER_IMAGE):arm32v7-$${version} \
|
||||
$(DOCKER_IMAGE):arm64v8-$${version} && \
|
||||
docker manifest annotate $(DOCKER_IMAGE):$${version} \
|
||||
$(DOCKER_IMAGE):arm32v6-$${version} --os linux --arch arm --variant v6 && \
|
||||
docker manifest annotate $(DOCKER_IMAGE):$${version} \
|
||||
$(DOCKER_IMAGE):arm32v7-$${version} --os linux --arch arm --variant v7 && \
|
||||
docker manifest annotate $(DOCKER_IMAGE):$${version} \
|
||||
$(DOCKER_IMAGE):arm64v8-$${version} --os linux --arch arm64 --variant v8 && \
|
||||
docker manifest push --purge $(DOCKER_IMAGE):$${version} ;\
|
||||
done
|
||||
debian-packages: clean
|
||||
$(MAKE) debian DEB_IMG_ARCH=amd64
|
||||
$(MAKE) debian DEB_IMG_ARCH=arm64v8
|
||||
$(MAKE) debian DEB_IMG_ARCH=arm32v7
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
Miniflux 2
|
||||
==========
|
||||
[](https://godoc.org/miniflux.app)
|
||||
|
||||
Miniflux is a minimalist and opinionated feed reader:
|
||||
|
||||
@@ -51,5 +50,5 @@ 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
|
||||
|
||||
@@ -29,12 +29,14 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa
|
||||
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("/discover", handler.getSubscriptions).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds", handler.createFeed).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds", handler.getFeeds).Methods(http.MethodGet)
|
||||
@@ -44,6 +46,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa
|
||||
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)
|
||||
|
||||
@@ -7,6 +7,7 @@ package api // import "miniflux.app/api"
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
@@ -64,6 +65,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 {
|
||||
|
||||
+24
-54
@@ -58,61 +58,16 @@ func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *handler) getFeedEntries(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
|
||||
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})
|
||||
h.findEntries(w, r, feedID)
|
||||
}
|
||||
|
||||
func (h *handler) getEntries(w http.ResponseWriter, r *http.Request) {
|
||||
status := request.QueryStringParam(r, "status", "")
|
||||
if status != "" {
|
||||
h.findEntries(w, r, 0)
|
||||
}
|
||||
|
||||
func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int64) {
|
||||
statuses := request.QueryStringParamList(r, "status")
|
||||
for _, status := range statuses {
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
@@ -138,8 +93,23 @@ func (h *handler) getEntries(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
|
||||
builder.WithStatus(status)
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.QueryInt64Param(r, "category_id", 0)
|
||||
if categoryID > 0 && !h.store.CategoryExists(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)
|
||||
|
||||
+27
@@ -7,6 +7,7 @@ package api // import "miniflux.app/api"
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
@@ -51,6 +52,9 @@ func (h *handler) createFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedInfo.Password,
|
||||
feedInfo.ScraperRules,
|
||||
feedInfo.RewriteRules,
|
||||
feedInfo.BlocklistRules,
|
||||
feedInfo.KeeplistRules,
|
||||
feedInfo.FetchViaProxy,
|
||||
)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
@@ -139,6 +143,29 @@ 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) getFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
feeds, err := h.store.Feeds(request.UserID(r))
|
||||
if err != nil {
|
||||
|
||||
@@ -89,6 +89,12 @@ func (m *middleware) basicAuth(next http.Handler) http.Handler {
|
||||
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][BasicAuth] [ClientIP=%s] Invalid username or password: %s", clientIP, username)
|
||||
json.Unauthorized(w, r)
|
||||
|
||||
+37
-23
@@ -24,35 +24,41 @@ type entriesResponse struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type subscriptionDiscovery struct {
|
||||
URL string `json:"url"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
URL string `json:"url"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
}
|
||||
|
||||
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"`
|
||||
Disabled *bool `json:"disabled"`
|
||||
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"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
Disabled *bool `json:"disabled"`
|
||||
}
|
||||
|
||||
func (f *feedModification) Update(feed *model.Feed) {
|
||||
@@ -76,6 +82,14 @@ func (f *feedModification) Update(feed *model.Feed) {
|
||||
feed.RewriteRules = *f.RewriteRules
|
||||
}
|
||||
|
||||
if f.KeeplistRules != nil {
|
||||
feed.KeeplistRules = *f.KeeplistRules
|
||||
}
|
||||
|
||||
if f.BlocklistRules != nil {
|
||||
feed.BlocklistRules = *f.BlocklistRules
|
||||
}
|
||||
|
||||
if f.Crawler != nil {
|
||||
feed.Crawler = *f.Crawler
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ func (h *handler) getSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
subscriptionInfo.UserAgent,
|
||||
subscriptionInfo.Username,
|
||||
subscriptionInfo.Password,
|
||||
subscriptionInfo.FetchViaProxy,
|
||||
)
|
||||
if finderErr != nil {
|
||||
json.ServerError(w, r, finderErr)
|
||||
|
||||
+20
@@ -92,6 +92,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)
|
||||
|
||||
+7
-15
@@ -9,12 +9,12 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/metric"
|
||||
"miniflux.app/reader/feed"
|
||||
"miniflux.app/service/httpd"
|
||||
"miniflux.app/service/scheduler"
|
||||
@@ -32,9 +32,7 @@ func startDaemon(store *storage.Storage) {
|
||||
feedHandler := feed.NewFeedHandler(store)
|
||||
pool := worker.NewPool(feedHandler, config.Opts.WorkerPoolSize())
|
||||
|
||||
go showProcessStatistics()
|
||||
|
||||
if config.Opts.HasSchedulerService() {
|
||||
if config.Opts.HasSchedulerService() && !config.Opts.HasMaintenanceMode() {
|
||||
scheduler.Serve(store, pool)
|
||||
}
|
||||
|
||||
@@ -43,6 +41,11 @@ func startDaemon(store *storage.Storage) {
|
||||
httpServer = httpd.Serve(store, pool, feedHandler)
|
||||
}
|
||||
|
||||
if config.Opts.HasMetricsCollector() {
|
||||
collector := metric.NewCollector(store, config.Opts.MetricsRefreshInterval())
|
||||
go collector.GatherStorageMetrics()
|
||||
}
|
||||
|
||||
<-stop
|
||||
logger.Info("Shutting down the process...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -54,14 +57,3 @@ func startDaemon(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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
Miniflux API Client
|
||||
===================
|
||||
|
||||
[](https://pkg.go.dev/miniflux.app/client)
|
||||
|
||||
Client library for Miniflux REST API.
|
||||
|
||||
Installation
|
||||
|
||||
@@ -133,6 +133,12 @@ func (c *Client) DeleteUser(userID int64) error {
|
||||
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.
|
||||
func (c *Client) Discover(url string) (Subscriptions, error) {
|
||||
body, err := c.request.Post("/v1/discover", map[string]string{"url": url})
|
||||
@@ -207,6 +213,12 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// DeleteCategory removes a category.
|
||||
func (c *Client) DeleteCategory(categoryID int64) error {
|
||||
return c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
|
||||
@@ -309,6 +321,12 @@ func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModification) (*Feed,
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// 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(fmt.Sprintf("/v1/feeds/refresh"), nil)
|
||||
@@ -480,6 +498,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())
|
||||
}
|
||||
|
||||
|
||||
+32
-24
@@ -92,6 +92,8 @@ type Feed struct {
|
||||
ParsingErrorCount int `json:"parsing_error_count,omitempty"`
|
||||
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"`
|
||||
Username string `json:"username"`
|
||||
@@ -101,16 +103,18 @@ type Feed struct {
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
}
|
||||
|
||||
// FeedIcon represents the feed icon.
|
||||
@@ -125,20 +129,22 @@ 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"`
|
||||
ShareCode string `json:"share_code"`
|
||||
Starred bool `json:"starred"`
|
||||
Enclosures Enclosures `json:"enclosures,omitempty"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
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.
|
||||
@@ -171,6 +177,8 @@ type Filter struct {
|
||||
AfterEntryID int64
|
||||
Search string
|
||||
CategoryID int64
|
||||
FeedID int64
|
||||
Statuses []string
|
||||
}
|
||||
|
||||
// EntryResultSet represents the response when fetching entries.
|
||||
|
||||
@@ -480,24 +480,6 @@ func TestCleanupFrequencyHours(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeprecatedCleanupFrequencyHoursVar(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("CLEANUP_FREQUENCY", "42")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := 42
|
||||
result := opts.CleanupFrequencyHours()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected CLEANUP_FREQUENCY value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCleanupArchiveReadDaysValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
@@ -534,24 +516,6 @@ func TestCleanupArchiveReadDays(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeprecatedCleanupArchiveReadDaysVar(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("ARCHIVE_READ_DAYS", "7")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := 7
|
||||
result := opts.CleanupArchiveReadDays()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected ARCHIVE_READ_DAYS value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCleanupRemoveSessionsDaysValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ const (
|
||||
defaultCertCache = "/tmp/cert_cache"
|
||||
defaultCleanupFrequencyHours = 24
|
||||
defaultCleanupArchiveReadDays = 60
|
||||
defaultCleanupArchiveUnreadDays = 180
|
||||
defaultCleanupRemoveSessionsDays = 30
|
||||
defaultProxyImages = "http-only"
|
||||
defaultCreateAdmin = false
|
||||
@@ -50,8 +51,15 @@ const (
|
||||
defaultPocketConsumerKey = ""
|
||||
defaultHTTPClientTimeout = 20
|
||||
defaultHTTPClientMaxBodySize = 15
|
||||
defaultHTTPClientProxy = ""
|
||||
defaultHTTPClientUserAgent = ""
|
||||
defaultAuthProxyHeader = ""
|
||||
defaultAuthProxyUserCreation = false
|
||||
defaultMaintenanceMode = false
|
||||
defaultMaintenanceMessage = "Miniflux is currently under maintenance"
|
||||
defaultMetricsCollector = false
|
||||
defaultMetricsRefreshInterval = 60
|
||||
defaultMetricsAllowedNetworks = "127.0.0.1/8"
|
||||
)
|
||||
|
||||
// Options contains configuration options.
|
||||
@@ -76,6 +84,7 @@ type Options struct {
|
||||
certKeyFile string
|
||||
cleanupFrequencyHours int
|
||||
cleanupArchiveReadDays int
|
||||
cleanupArchiveUnreadDays int
|
||||
cleanupRemoveSessionsDays int
|
||||
pollingFrequency int
|
||||
batchSize int
|
||||
@@ -96,8 +105,15 @@ type Options struct {
|
||||
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.
|
||||
@@ -123,6 +139,7 @@ func NewOptions() *Options {
|
||||
certKeyFile: defaultKeyFile,
|
||||
cleanupFrequencyHours: defaultCleanupFrequencyHours,
|
||||
cleanupArchiveReadDays: defaultCleanupArchiveReadDays,
|
||||
cleanupArchiveUnreadDays: defaultCleanupArchiveUnreadDays,
|
||||
cleanupRemoveSessionsDays: defaultCleanupRemoveSessionsDays,
|
||||
pollingFrequency: defaultPollingFrequency,
|
||||
batchSize: defaultBatchSize,
|
||||
@@ -141,8 +158,15 @@ func NewOptions() *Options {
|
||||
pocketConsumerKey: defaultPocketConsumerKey,
|
||||
httpClientTimeout: defaultHTTPClientTimeout,
|
||||
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
|
||||
httpClientProxy: defaultHTTPClientProxy,
|
||||
authProxyHeader: defaultAuthProxyHeader,
|
||||
authProxyUserCreation: defaultAuthProxyUserCreation,
|
||||
maintenanceMode: defaultMaintenanceMode,
|
||||
maintenanceMessage: defaultMaintenanceMessage,
|
||||
metricsCollector: defaultMetricsCollector,
|
||||
metricsRefreshInterval: defaultMetricsRefreshInterval,
|
||||
metricsAllowedNetworks: []string{defaultMetricsAllowedNetworks},
|
||||
httpClientUserAgent: defaultHTTPClientUserAgent,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +175,16 @@ 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
|
||||
@@ -226,6 +260,11 @@ 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
|
||||
@@ -349,6 +388,16 @@ 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 {
|
||||
@@ -361,6 +410,26 @@ 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
|
||||
}
|
||||
|
||||
func (o *Options) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString(fmt.Sprintf("LOG_DATE_TIME: %v\n", o.logDateTime))
|
||||
@@ -383,6 +452,7 @@ func (o *Options) String() string {
|
||||
builder.WriteString(fmt.Sprintf("CERT_CACHE: %v\n", o.certCache))
|
||||
builder.WriteString(fmt.Sprintf("CLEANUP_FREQUENCY_HOURS: %v\n", o.cleanupFrequencyHours))
|
||||
builder.WriteString(fmt.Sprintf("CLEANUP_ARCHIVE_READ_DAYS: %v\n", o.cleanupArchiveReadDays))
|
||||
builder.WriteString(fmt.Sprintf("CLEANUP_ARCHIVE_UNREAD_DAYS: %v\n", o.cleanupArchiveUnreadDays))
|
||||
builder.WriteString(fmt.Sprintf("CLEANUP_REMOVE_SESSIONS_DAYS: %v\n", o.cleanupRemoveSessionsDays))
|
||||
builder.WriteString(fmt.Sprintf("WORKER_POOL_SIZE: %v\n", o.workerPoolSize))
|
||||
builder.WriteString(fmt.Sprintf("POLLING_FREQUENCY: %v\n", o.pollingFrequency))
|
||||
@@ -403,7 +473,14 @@ func (o *Options) String() string {
|
||||
builder.WriteString(fmt.Sprintf("OAUTH2_PROVIDER: %v\n", o.oauth2Provider))
|
||||
builder.WriteString(fmt.Sprintf("HTTP_CLIENT_TIMEOUT: %v\n", o.httpClientTimeout))
|
||||
builder.WriteString(fmt.Sprintf("HTTP_CLIENT_MAX_BODY_SIZE: %v\n", o.httpClientMaxBodySize))
|
||||
builder.WriteString(fmt.Sprintf("HTTP_CLIENT_PROXY: %v\n", o.httpClientProxy))
|
||||
builder.WriteString(fmt.Sprintf("HTTP_CLIENT_USER_AGENT: %v\n", o.httpClientUserAgent))
|
||||
builder.WriteString(fmt.Sprintf("AUTH_PROXY_HEADER: %v\n", o.authProxyHeader))
|
||||
builder.WriteString(fmt.Sprintf("AUTH_PROXY_USER_CREATION: %v\n", o.authProxyUserCreation))
|
||||
builder.WriteString(fmt.Sprintf("MAINTENANCE_MODE: %v\n", o.maintenanceMode))
|
||||
builder.WriteString(fmt.Sprintf("MAINTENANCE_MESSAGE: %v\n", o.maintenanceMessage))
|
||||
builder.WriteString(fmt.Sprintf("METRICS_COLLECTOR: %v\n", o.metricsCollector))
|
||||
builder.WriteString(fmt.Sprintf("METRICS_REFRESH_INTERVAL: %v\n", o.metricsRefreshInterval))
|
||||
builder.WriteString(fmt.Sprintf("METRICS_ALLOWED_NETWORKS: %v\n", o.metricsAllowedNetworks))
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
+30
-18
@@ -15,8 +15,6 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/logger"
|
||||
)
|
||||
|
||||
// Parser handles configuration parsing.
|
||||
@@ -118,24 +116,10 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
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 "CLEANUP_FREQUENCY":
|
||||
logger.Error("[Config] CLEANUP_FREQUENCY has been deprecated in favor of CLEANUP_FREQUENCY_HOURS.")
|
||||
|
||||
if p.opts.cleanupFrequencyHours != defaultCleanupFrequencyHours {
|
||||
logger.Error("[Config] Ignoring CLEANUP_FREQUENCY as CLEANUP_FREQUENCY_HOURS is already specified.")
|
||||
} else {
|
||||
p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
|
||||
}
|
||||
case "ARCHIVE_READ_DAYS":
|
||||
logger.Error("[Config] ARCHIVE_READ_DAYS has been deprecated in favor of CLEANUP_ARCHIVE_READ_DAYS.")
|
||||
|
||||
if p.opts.cleanupArchiveReadDays != defaultCleanupArchiveReadDays {
|
||||
logger.Error("[Config] Ignoring ARCHIVE_READ_DAYS as CLEANUP_ARCHIVE_READ_DAYS is already specified.")
|
||||
} else {
|
||||
p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays)
|
||||
}
|
||||
case "WORKER_POOL_SIZE":
|
||||
p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
|
||||
case "POLLING_FREQUENCY":
|
||||
@@ -184,10 +168,24 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +252,20 @@ func parseString(value string, fallback string) string {
|
||||
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 := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
|
||||
@@ -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,25 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
miniflux:
|
||||
image: miniflux/miniflux:latest
|
||||
container_name: miniflux
|
||||
ports:
|
||||
- "80:8080"
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
|
||||
- RUN_MIGRATIONS=1
|
||||
- CREATE_ADMIN=1
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=test123
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
volumes:
|
||||
miniflux-db:
|
||||
@@ -0,0 +1,38 @@
|
||||
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
|
||||
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
|
||||
volumes:
|
||||
miniflux-db:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
@@ -0,0 +1,48 @@
|
||||
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
|
||||
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
|
||||
volumes:
|
||||
miniflux-db:
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"miniflux.app/logger"
|
||||
)
|
||||
|
||||
const schemaVersion = 34
|
||||
const schemaVersion = 42
|
||||
|
||||
// Migrate executes database migrations.
|
||||
func Migrate(db *sql.DB) {
|
||||
|
||||
@@ -187,8 +187,25 @@ create index entries_user_feed_idx on entries (user_id, feed_id);
|
||||
`,
|
||||
"schema_version_33": `alter table users add column show_reading_time boolean default 't';`,
|
||||
"schema_version_34": `CREATE INDEX entries_id_user_status_idx ON entries USING btree (id, user_id, status);`,
|
||||
"schema_version_35": `alter table feeds add column fetch_via_proxy bool default false;
|
||||
`,
|
||||
"schema_version_36": `CREATE INDEX entries_feed_id_status_hash_idx ON entries USING btree (feed_id, status, hash);`,
|
||||
"schema_version_37": `CREATE INDEX entries_user_id_status_starred_idx ON entries (user_id, status, starred);`,
|
||||
"schema_version_38": `alter table users add column entry_swipe boolean default 't';
|
||||
`,
|
||||
"schema_version_39": `ALTER TABLE integrations DROP COLUMN fever_password;
|
||||
`,
|
||||
"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_40": `alter table feeds
|
||||
add column blocklist_rules text not null default '',
|
||||
add column keeplist_rules text not null default ''
|
||||
;
|
||||
`,
|
||||
"schema_version_41": `alter table entries add column reading_time int not null default 0;`,
|
||||
"schema_version_42": `alter table entries add column created_at timestamp with time zone not null default now();
|
||||
update entries set created_at = published_at;
|
||||
`,
|
||||
"schema_version_5": `create table integrations (
|
||||
user_id int not null,
|
||||
@@ -244,7 +261,15 @@ var SqlMapChecksums = map[string]string{
|
||||
"schema_version_32": "5b4de8dd2d7e3c6ae4150e0e3931df2ee989f2c667145bd67294e5a5f3fae456",
|
||||
"schema_version_33": "bf38514efeb6c12511f41b1cc484f92722240b0a6ae874c32a958dfea3433d02",
|
||||
"schema_version_34": "1a3e036f652fc98b7564a27013f04e1eb36dd0d68893c723168f134dc1065822",
|
||||
"schema_version_35": "162a55df78eed4b9c9c141878132d5f1d97944b96f35a79e38f55716cdd6b3d2",
|
||||
"schema_version_36": "8164be7818268ad3d4bdcad03a7868b58e32b27cde9b4f056cd82f7b182a0722",
|
||||
"schema_version_37": "fc9eb1b452341664ddf24c1a9cf01502ac2578136e54a4853081652959285cb9",
|
||||
"schema_version_38": "e91d2f4075ceb7b8a16a25f350f36dee12cfd1ad86b8b6414c4cf2e9a003358c",
|
||||
"schema_version_39": "b0f90b97502921d4681a07c64d180a91a0b4ccac7d3c1dbe30519ad6f1bf1737",
|
||||
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
|
||||
"schema_version_40": "6a8fec92399f853ed6817aff4cfa43255dce4c19afad796e41519d09de62105e",
|
||||
"schema_version_41": "128e118ce61267ea1f6ae03b63a6d4734eae87e520b00e309ad083f1f6afdfe5",
|
||||
"schema_version_42": "3d0cd422c6d9d13e7a619a8dbf081e17750881e0ae9ae380475b09d37ada9e33",
|
||||
"schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c",
|
||||
"schema_version_6": "9d05b4fb223f0e60efc716add5048b0ca9c37511cf2041721e20505d6d798ce4",
|
||||
"schema_version_7": "33f298c9aa30d6de3ca28e1270df51c2884d7596f1283a75716e2aeb634cd05c",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
alter table feeds add column fetch_via_proxy bool default false;
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX entries_feed_id_status_hash_idx ON entries USING btree (feed_id, status, hash);
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX entries_user_id_status_starred_idx ON entries (user_id, status, starred);
|
||||
@@ -0,0 +1 @@
|
||||
alter table users add column entry_swipe boolean default 't';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE integrations DROP COLUMN fever_password;
|
||||
@@ -0,0 +1,4 @@
|
||||
alter table feeds
|
||||
add column blocklist_rules text not null default '',
|
||||
add column keeplist_rules text not null default ''
|
||||
;
|
||||
@@ -0,0 +1 @@
|
||||
alter table entries add column reading_time int not null default 0;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table entries add column created_at timestamp with time zone not null default now();
|
||||
update entries set created_at = published_at;
|
||||
+2
-2
@@ -194,8 +194,8 @@ func main() {
|
||||
"ui/static/js/app.js",
|
||||
"ui/static/js/bootstrap.js",
|
||||
},
|
||||
"sw": []string{
|
||||
"ui/static/js/sw.js",
|
||||
"service-worker": []string{
|
||||
"ui/static/js/service_worker.js",
|
||||
},
|
||||
}, map[string]string{
|
||||
"app": "(function(){'use strict';",
|
||||
|
||||
@@ -3,18 +3,20 @@ module miniflux.app
|
||||
// +heroku goVersion go1.11
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.5.1
|
||||
github.com/PuerkitoBio/goquery v1.6.0
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/golang/protobuf v1.4.0 // indirect
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/lib/pq v1.8.0
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.7.4 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0
|
||||
github.com/prometheus/client_golang v1.8.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.10 // indirect
|
||||
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
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@@ -1,125 +1,468 @@
|
||||
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/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
|
||||
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/PuerkitoBio/goquery v1.6.0 h1:j7taAbelrdcsOlGeMenZxc2AWXD5fieT1/znArdnx94=
|
||||
github.com/PuerkitoBio/goquery v1.6.0/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/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/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/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/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/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/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
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/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/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.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
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.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/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-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
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 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
|
||||
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 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
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-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.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 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.2.0 h1:lzPl/30ZLkTveYsYZPKMcgXc8MbnE6RsTd4F9KgiLtk=
|
||||
github.com/jcmturner/gokrb5/v8 v8.2.0/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZgZdoFrZaZNM=
|
||||
github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0=
|
||||
github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc=
|
||||
github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.6.0 h1:I5DPxhYJChW9KYc66se+oKFFQX6VuQrKiprsX6ivRZc=
|
||||
github.com/lib/pq v1.6.0/go.mod h1:4vXEAYvW1fRQ2/FhZ78H73A60MHw1geSm145z2mdY1g=
|
||||
github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY=
|
||||
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.7.1 h1:FvD5XTVTDt+KON6oIoOmHq6B6HzGuYEhuTMpEG0yuBQ=
|
||||
github.com/lib/pq v1.7.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/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/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
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/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.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/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/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
|
||||
github.com/lib/pq v1.8.0/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/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
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-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
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-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 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/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/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
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/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
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 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 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw=
|
||||
github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM=
|
||||
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.2.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 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4=
|
||||
github.com/prometheus/common v0.14.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.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 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
|
||||
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
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/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/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/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
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 v1.1.0 h1:nxHQi1ML+g3ZbZHffiZ6eC7vMqNvSRfX3KB5Y5y/kfw=
|
||||
github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo=
|
||||
github.com/tdewolff/minify/v2 v2.7.4 h1:r0OZQ3QzWeDS5cXq53Bk4IFIBDZ7fiXIkw1a4bHONsw=
|
||||
github.com/tdewolff/minify/v2 v2.7.4/go.mod h1:BkDSm8aMMT0ALGmpt7j3Ra7nLUgZL0qhyrAHXwxcy5w=
|
||||
github.com/tdewolff/parse/v2 v2.4.2 h1:Bu2Qv6wepkc+Ou7iB/qHjAhEImlAP5vedzlQRUdj3BI=
|
||||
github.com/tdewolff/parse/v2 v2.4.2/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
github.com/tdewolff/minify/v2 v2.9.10 h1:p+ifTTl+JMFFLDYNAm7nxQ9XuCG10HTW00wlPAZ7aoE=
|
||||
github.com/tdewolff/minify/v2 v2.9.10/go.mod h1:U1Nc+/YBSB0FPEarqcgkYH3Ep4DNyyIbOyl5P4eWMuo=
|
||||
github.com/tdewolff/parse/v2 v2.5.5 h1:b7ICJa4I/54JQGEGgTte8DiyJPKcC5g8V773QMzkeUM=
|
||||
github.com/tdewolff/parse/v2 v2.5.5/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
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/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=
|
||||
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.uber.org/atomic v1.3.2/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/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-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 h1:Q7tZBpemrlsc2I7IyODzhtallWRSm4Q0d09pL6XbQtU=
|
||||
golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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-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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
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-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-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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/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 h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
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-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-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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
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-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88=
|
||||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
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-20180412165947-fbb02b2291d2/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-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/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
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.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
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.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 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
|
||||
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
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=
|
||||
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=
|
||||
|
||||
+124
-49
@@ -6,7 +6,6 @@ package client // import "miniflux.app/http/client"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -26,6 +25,11 @@ import (
|
||||
"miniflux.app/version"
|
||||
)
|
||||
|
||||
const (
|
||||
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)"
|
||||
@@ -36,72 +40,116 @@ var (
|
||||
errRequestTimeout = "Website unreachable, the request timed out after %d seconds"
|
||||
)
|
||||
|
||||
// Client is a HTTP Client :)
|
||||
// Client builds and executes HTTP requests.
|
||||
type Client struct {
|
||||
inputURL string
|
||||
requestURL 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
|
||||
|
||||
useProxy bool
|
||||
doNotFollowRedirects bool
|
||||
|
||||
ClientTimeout int
|
||||
ClientMaxBodySize int64
|
||||
ClientProxyURL string
|
||||
}
|
||||
|
||||
// New initializes a new HTTP client.
|
||||
func New(url string) *Client {
|
||||
return &Client{
|
||||
inputURL: url,
|
||||
requestUserAgent: DefaultUserAgent,
|
||||
ClientTimeout: defaultHTTPClientTimeout,
|
||||
ClientMaxBodySize: defaultHTTPClientMaxBodySize,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientWithConfig initializes a new HTTP client with application config options.
|
||||
func NewClientWithConfig(url string, opts *config.Options) *Client {
|
||||
userAgent := opts.HTTPClientUserAgent()
|
||||
if userAgent == "" {
|
||||
userAgent = DefaultUserAgent
|
||||
}
|
||||
return &Client{
|
||||
inputURL: url,
|
||||
requestUserAgent: userAgent,
|
||||
ClientTimeout: opts.HTTPClientTimeout(),
|
||||
ClientMaxBodySize: opts.HTTPClientMaxBodySize(),
|
||||
ClientProxyURL: opts.HTTPClientProxy(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) String() string {
|
||||
etagHeader := c.etagHeader
|
||||
if c.etagHeader == "" {
|
||||
etagHeader := c.requestEtagHeader
|
||||
if c.requestEtagHeader == "" {
|
||||
etagHeader = "None"
|
||||
}
|
||||
|
||||
lastModifiedHeader := c.lastModifiedHeader
|
||||
if c.lastModifiedHeader == "" {
|
||||
lastModifiedHeader := c.requestLastModifiedHeader
|
||||
if c.requestLastModifiedHeader == "" {
|
||||
lastModifiedHeader = "None"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`InputURL=%q RequestURL=%q ETag=%s LastModified=%s BasicAuth=%v UserAgent=%q`,
|
||||
`InputURL=%q RequestURL=%q ETag=%s LastModified=%s Auth=%v UserAgent=%q`,
|
||||
c.inputURL,
|
||||
c.requestURL,
|
||||
etagHeader,
|
||||
lastModifiedHeader,
|
||||
c.authorizationHeader != "" || (c.username != "" && c.password != ""),
|
||||
c.userAgent,
|
||||
c.requestAuthorizationHeader != "" || (c.requestUsername != "" && c.requestPassword != ""),
|
||||
c.requestUserAgent,
|
||||
)
|
||||
}
|
||||
|
||||
// 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.requestLastModifiedHeader = 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.
|
||||
// Get performs a GET HTTP request.
|
||||
func (c *Client) Get() (*Response, error) {
|
||||
request, err := c.buildRequest(http.MethodGet, nil)
|
||||
if err != nil {
|
||||
@@ -111,7 +159,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 {
|
||||
@@ -122,7 +170,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 {
|
||||
@@ -166,7 +214,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, config.Opts.HTTPClientTimeout())
|
||||
err = errors.NewLocalizedError(errRequestTimeout, c.ClientTimeout)
|
||||
} else if nerr.Temporary() {
|
||||
err = errors.NewLocalizedError(errTemporaryNetworkOperation, nerr)
|
||||
}
|
||||
@@ -176,7 +224,7 @@ func (c *Client) executeRequest(request *http.Request) (*Response, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.ContentLength > config.Opts.HTTPClientMaxBodySize() {
|
||||
if resp.ContentLength > c.ClientMaxBodySize {
|
||||
return nil, fmt.Errorf("client: response too large (%d bytes)", resp.ContentLength)
|
||||
}
|
||||
|
||||
@@ -221,46 +269,73 @@ func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, 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(config.Opts.HTTPClientTimeout()) * 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.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("User-Agent", c.requestUserAgent)
|
||||
headers.Add("Accept", "*/*")
|
||||
|
||||
if c.etagHeader != "" {
|
||||
headers.Add("If-None-Match", c.etagHeader)
|
||||
if c.requestEtagHeader != "" {
|
||||
headers.Add("If-None-Match", c.requestEtagHeader)
|
||||
}
|
||||
|
||||
if c.lastModifiedHeader != "" {
|
||||
headers.Add("If-Modified-Since", c.lastModifiedHeader)
|
||||
if c.requestLastModifiedHeader != "" {
|
||||
headers.Add("If-Modified-Since", c.requestLastModifiedHeader)
|
||||
}
|
||||
|
||||
if c.authorizationHeader != "" {
|
||||
headers.Add("Authorization", c.authorizationHeader)
|
||||
if c.requestAuthorizationHeader != "" {
|
||||
headers.Add("Authorization", c.requestAuthorizationHeader)
|
||||
}
|
||||
|
||||
headers.Add("Connection", "close")
|
||||
return headers
|
||||
}
|
||||
|
||||
// New returns a new HTTP client.
|
||||
func New(url string) *Client {
|
||||
return &Client{inputURL: url, userAgent: DefaultUserAgent, Insecure: false}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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 (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"miniflux.app/config"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRequestUserAgent(t *testing.T) {
|
||||
clt := New("http://httpbin.org")
|
||||
if clt.requestUserAgent != DefaultUserAgent {
|
||||
t.Errorf(`The client had default User-Agent %q, wanted %q`, clt.requestUserAgent, DefaultUserAgent)
|
||||
}
|
||||
|
||||
userAgent := "Custom User Agent"
|
||||
os.Setenv("HTTP_CLIENT_USER_AGENT", userAgent)
|
||||
opts, err := config.NewParser().ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing config failed: %v`, err)
|
||||
}
|
||||
clt = NewClientWithConfig("http://httpbin.org", opts)
|
||||
if clt.requestUserAgent != userAgent {
|
||||
t.Errorf(`The client had User-Agent %q, wanted %q`, clt.requestUserAgent, userAgent)
|
||||
}
|
||||
}
|
||||
+18
-24
@@ -87,32 +87,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 != "" {
|
||||
// JSON feeds are always in UTF-8.
|
||||
if strings.Contains(r.ContentType, "json") {
|
||||
return
|
||||
buffer, err := ioutil.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)
|
||||
}
|
||||
|
||||
if strings.Contains(r.ContentType, "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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ package request // import "miniflux.app/http/request"
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -52,6 +53,23 @@ func QueryStringParam(r *http.Request, param, defaultValue string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// QueryStringParamList returns all values associated to the parameter.
|
||||
func QueryStringParamList(r *http.Request, param string) []string {
|
||||
var results []string
|
||||
values := r.URL.Query()
|
||||
|
||||
if _, found := values[param]; found {
|
||||
for _, value := range values[param] {
|
||||
value = strings.TrimSpace(value)
|
||||
if value != "" {
|
||||
results = append(results, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// QueryIntParam returns a query string parameter as integer.
|
||||
func QueryIntParam(r *http.Request, param string, defaultValue int) int {
|
||||
return int(QueryInt64Param(r, param, int64(defaultValue)))
|
||||
|
||||
+94
-38
@@ -71,10 +71,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "Diesen Artikel speichern",
|
||||
"entry.save.completed": "Erledigt!",
|
||||
"entry.save.toast.completed": "Artikel gespeichert",
|
||||
"entry.scraper.label": "Inhalt herunterladen",
|
||||
"entry.scraper.label": "Herunterladen",
|
||||
"entry.scraper.title": "Inhalt herunterladen",
|
||||
"entry.scraper.completed": "Erledigt!",
|
||||
"entry.original.label": "Original-Artikel",
|
||||
"entry.external_link.label": "Externer Link",
|
||||
"entry.comments.label": "Kommentare",
|
||||
"entry.comments.title": "Kommentare anzeigen",
|
||||
"entry.share.label": "Teilen",
|
||||
@@ -159,6 +159,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "Vollständigen Inhalt herunterladen",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Lesezeichen hinzufügen/entfernen",
|
||||
"page.keyboard_shortcuts.save_article": "Artikel speichern",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Artikel nach oben blättern",
|
||||
"page.keyboard_shortcuts.remove_feed": "Dieses Abonnement entfernen",
|
||||
"page.keyboard_shortcuts.go_to_search": "Fokus auf das Suchformular setzen",
|
||||
"page.keyboard_shortcuts.close_modal": "Liste der Tastenkürzel schließen",
|
||||
@@ -253,7 +254,10 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "Standardbenutzeragenten überschreiben",
|
||||
"form.feed.label.scraper_rules": "Extraktionsregeln",
|
||||
"form.feed.label.rewrite_rules": "Umschreiberegeln",
|
||||
"form.feed.label.blocklist_rules": "Regeln blockieren",
|
||||
"form.feed.label.keeplist_rules": "Regeln einhalten",
|
||||
"form.feed.label.ignore_http_cache": "Ignoriere HTTP-cache",
|
||||
"form.feed.label.fetch_via_proxy": "Über Proxy abrufen",
|
||||
"form.feed.label.disabled": "Dieses Abonnement nicht aktualisieren",
|
||||
"form.category.label.title": "Titel",
|
||||
"form.user.label.username": "Benutzername",
|
||||
@@ -268,6 +272,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "Älteste Artikel zuerst",
|
||||
"form.prefs.select.recent_first": "Neueste Artikel zuerst",
|
||||
"form.prefs.label.keyboard_shortcuts": "Tastaturkürzel aktivieren",
|
||||
"form.prefs.label.entry_swipe": "Wischgeste für Einträge auf dem Handy aktivieren",
|
||||
"form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
|
||||
"form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
|
||||
"form.import.label.file": "OPML Datei",
|
||||
@@ -416,10 +421,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "Save this article",
|
||||
"entry.save.completed": "Done!",
|
||||
"entry.save.toast.completed": "Article saved",
|
||||
"entry.scraper.label": "Original",
|
||||
"entry.scraper.label": "Download",
|
||||
"entry.scraper.title": "Fetch original content",
|
||||
"entry.scraper.completed": "Done!",
|
||||
"entry.original.label": "Original",
|
||||
"entry.external_link.label": "External link",
|
||||
"entry.comments.label": "Comments",
|
||||
"entry.comments.title": "View Comments",
|
||||
"entry.share.label": "Share",
|
||||
@@ -504,6 +509,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "Download original content",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Toggle bookmark",
|
||||
"page.keyboard_shortcuts.save_article": "Save article",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll item to top",
|
||||
"page.keyboard_shortcuts.remove_feed": "Remove this feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "Set focus on search form",
|
||||
"page.keyboard_shortcuts.close_modal": "Close modal dialog",
|
||||
@@ -598,7 +604,10 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "Override Default User Agent",
|
||||
"form.feed.label.scraper_rules": "Scraper Rules",
|
||||
"form.feed.label.rewrite_rules": "Rewrite Rules",
|
||||
"form.feed.label.blocklist_rules": "Block Rules",
|
||||
"form.feed.label.keeplist_rules": "Keep Rules",
|
||||
"form.feed.label.ignore_http_cache": "Ignore HTTP cache",
|
||||
"form.feed.label.fetch_via_proxy": "Fetch via proxy",
|
||||
"form.feed.label.disabled": "Do not refresh this feed",
|
||||
"form.category.label.title": "Title",
|
||||
"form.user.label.username": "Username",
|
||||
@@ -613,6 +622,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "Older entries first",
|
||||
"form.prefs.select.recent_first": "Recent entries first",
|
||||
"form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts",
|
||||
"form.prefs.label.entry_swipe": "Enable swipe gesture on entries on mobile",
|
||||
"form.prefs.label.show_reading_time": "Show estimated reading time for articles",
|
||||
"form.prefs.label.custom_css": "Custom CSS",
|
||||
"form.import.label.file": "OPML file",
|
||||
@@ -741,10 +751,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "Guardar este articulo",
|
||||
"entry.save.completed": "¡Hecho!",
|
||||
"entry.save.toast.completed": "Artículo guardado",
|
||||
"entry.scraper.label": "Obtener contenido original",
|
||||
"entry.scraper.label": "Descargar",
|
||||
"entry.scraper.title": "Obtener contenido original",
|
||||
"entry.scraper.completed": "¡Hecho!",
|
||||
"entry.original.label": "Original",
|
||||
"entry.external_link.label": "Enlace externo",
|
||||
"entry.comments.label": "Comentarios",
|
||||
"entry.comments.title": "Ver comentarios",
|
||||
"entry.share.label": "Comparta",
|
||||
@@ -829,6 +839,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "Descargar el contento original",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Agregar o quitar marcador",
|
||||
"page.keyboard_shortcuts.save_article": "Guardar artículo",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Desplazar elemento hacia arriba",
|
||||
"page.keyboard_shortcuts.remove_feed": "Quitar esta fuente",
|
||||
"page.keyboard_shortcuts.go_to_search": "Centrarse en el cuadro de búsqueda",
|
||||
"page.keyboard_shortcuts.close_modal": "Cerrar el cuadro de diálogo modal",
|
||||
@@ -923,7 +934,11 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "Invalidar el agente de usuario predeterminado",
|
||||
"form.feed.label.scraper_rules": "Reglas de raspador",
|
||||
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
||||
"form.feed.label.blocklist_rules": "Reglas de Filtrado(Bloquear)",
|
||||
"form.feed.label.keeplist_rules": "Reglas de Filtrado(Permitir)",
|
||||
"form.feed.label.blocklist_rules": "Reglas de Blacklist",
|
||||
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
||||
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
||||
"form.feed.label.disabled": "No actualice este feed",
|
||||
"form.category.label.title": "Título",
|
||||
"form.user.label.username": "Nombre de usuario",
|
||||
@@ -938,6 +953,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "Entradas más viejas primero",
|
||||
"form.prefs.select.recent_first": "Entradas recientes primero",
|
||||
"form.prefs.label.keyboard_shortcuts": "Habilitar atajos de teclado",
|
||||
"form.prefs.label.entry_swipe": "Habilitar el gesto de deslizar el dedo en las entradas en el móvil",
|
||||
"form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
|
||||
"form.prefs.label.custom_css": "CSS personalizado",
|
||||
"form.import.label.file": "Archivo OPML",
|
||||
@@ -1066,10 +1082,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "Sauvegarder cet article",
|
||||
"entry.save.completed": "Terminé !",
|
||||
"entry.save.toast.completed": "Article sauvegardé",
|
||||
"entry.scraper.label": "Original",
|
||||
"entry.scraper.label": "Télécharger",
|
||||
"entry.scraper.title": "Récupérer le contenu original",
|
||||
"entry.scraper.completed": "Terminé !",
|
||||
"entry.original.label": "Original",
|
||||
"entry.external_link.label": "Lien externe",
|
||||
"entry.comments.label": "Commentaires",
|
||||
"entry.comments.title": "Voir les commentaires",
|
||||
"entry.share.label": "Partager",
|
||||
@@ -1154,6 +1170,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "Télécharger le contenu original",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Ajouter/Enlever favoris",
|
||||
"page.keyboard_shortcuts.save_article": "Sauvegarder l'article",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Faire défiler l'élément vers le haut",
|
||||
"page.keyboard_shortcuts.remove_feed": "Supprimer ce flux",
|
||||
"page.keyboard_shortcuts.go_to_search": "Mettre le focus sur le champ de recherche",
|
||||
"page.keyboard_shortcuts.close_modal": "Fermer la boite de dialogue",
|
||||
@@ -1248,7 +1265,10 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "Remplacer l'agent utilisateur par défaut",
|
||||
"form.feed.label.scraper_rules": "Règles pour récupérer le contenu original",
|
||||
"form.feed.label.rewrite_rules": "Règles de réécriture",
|
||||
"form.feed.label.blocklist_rules": "Règles de blocage",
|
||||
"form.feed.label.keeplist_rules": "Règles d'autorisation",
|
||||
"form.feed.label.ignore_http_cache": "Ignore cache HTTP",
|
||||
"form.feed.label.fetch_via_proxy": "Récupérer via proxy",
|
||||
"form.feed.label.disabled": "Ne pas actualiser ce flux",
|
||||
"form.category.label.title": "Titre",
|
||||
"form.user.label.username": "Nom d'utilisateur",
|
||||
@@ -1263,6 +1283,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "Ancien éléments en premier",
|
||||
"form.prefs.select.recent_first": "Éléments récents en premier",
|
||||
"form.prefs.label.keyboard_shortcuts": "Activer les raccourcis clavier",
|
||||
"form.prefs.label.entry_swipe": "Activer le geste de balayage sur les entrées sur mobile",
|
||||
"form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles",
|
||||
"form.prefs.label.custom_css": "CSS personnalisé",
|
||||
"form.import.label.file": "Fichier OPML",
|
||||
@@ -1411,10 +1432,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "Salva questo articolo",
|
||||
"entry.save.completed": "Fatto!",
|
||||
"entry.save.toast.completed": "Articolo salvato",
|
||||
"entry.scraper.label": "Scarica il contenuto integrale",
|
||||
"entry.scraper.label": "Scarica",
|
||||
"entry.scraper.title": "Scarica il contenuto integrale",
|
||||
"entry.scraper.completed": "Fatto!",
|
||||
"entry.original.label": "Originale",
|
||||
"entry.external_link.label": "Link esterno",
|
||||
"entry.comments.label": "Commenti",
|
||||
"entry.comments.title": "Mostra i commenti",
|
||||
"entry.share.label": "Condividi",
|
||||
@@ -1499,6 +1520,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "Scarica il contenuto integrale",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Aggiungi/rimuovi dai preferiti",
|
||||
"page.keyboard_shortcuts.save_article": "Salva l'articolo",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Scorri l'articolo in alto",
|
||||
"page.keyboard_shortcuts.remove_feed": "Rimuovi questo feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "Apri la casella di ricerca",
|
||||
"page.keyboard_shortcuts.close_modal": "Chiudi la finestra di dialogo",
|
||||
@@ -1593,7 +1615,10 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "Usa user agent personalizzato",
|
||||
"form.feed.label.scraper_rules": "Regole di estrazione del contenuto",
|
||||
"form.feed.label.rewrite_rules": "Regole di impaginazione del contenuto",
|
||||
"form.feed.label.blocklist_rules": "Regole di blocco",
|
||||
"form.feed.label.keeplist_rules": "Regole di autorizzazione",
|
||||
"form.feed.label.ignore_http_cache": "Ignora cache HTTP",
|
||||
"form.feed.label.fetch_via_proxy": "Recuperare tramite proxy",
|
||||
"form.feed.label.disabled": "Non aggiornare questo feed",
|
||||
"form.category.label.title": "Titolo",
|
||||
"form.user.label.username": "Nome utente",
|
||||
@@ -1608,6 +1633,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "Prima i più vecchi",
|
||||
"form.prefs.select.recent_first": "Prima i più recenti",
|
||||
"form.prefs.label.keyboard_shortcuts": "Abilita le scorciatoie da tastiera",
|
||||
"form.prefs.label.entry_swipe": "Abilita il gesto di scorrimento sulle voci sul cellulare",
|
||||
"form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
|
||||
"form.prefs.label.custom_css": "CSS personalizzati",
|
||||
"form.import.label.file": "File OPML",
|
||||
@@ -1736,10 +1762,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "この記事を保存",
|
||||
"entry.save.completed": "完了!",
|
||||
"entry.save.toast.completed": "記事は保存されました",
|
||||
"entry.scraper.label": "オリジナルの内容を取得",
|
||||
"entry.scraper.label": "ダウンロード",
|
||||
"entry.scraper.title": "オリジナルの内容を取得",
|
||||
"entry.scraper.completed": "完了!",
|
||||
"entry.original.label": "オリジナル",
|
||||
"entry.external_link.label": "外部リンク",
|
||||
"entry.comments.label": "コメント",
|
||||
"entry.comments.title": "コメントを見る",
|
||||
"entry.share.label": "共有",
|
||||
@@ -1824,6 +1850,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "オリジナルの内容をダウンロード",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "星を付ける/外す",
|
||||
"page.keyboard_shortcuts.save_article": "記事を保存",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "アイテムを上にスクロール",
|
||||
"page.keyboard_shortcuts.remove_feed": "このフィードを削除",
|
||||
"page.keyboard_shortcuts.go_to_search": "検索フォームにフォーカスを移す",
|
||||
"page.keyboard_shortcuts.close_modal": "モーダルダイアログを閉じる",
|
||||
@@ -1918,7 +1945,10 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "ディフォルトの User Agent を上書きする",
|
||||
"form.feed.label.scraper_rules": "スクラップルール",
|
||||
"form.feed.label.rewrite_rules": "Rewrite ルール",
|
||||
"form.feed.label.blocklist_rules": "ブロックルール",
|
||||
"form.feed.label.keeplist_rules": "許可規則",
|
||||
"form.feed.label.ignore_http_cache": "HTTPキャッシュを無視",
|
||||
"form.feed.label.fetch_via_proxy": "プロキシ経由でフェッチ",
|
||||
"form.feed.label.disabled": "このフィードを更新しない",
|
||||
"form.category.label.title": "タイトル",
|
||||
"form.user.label.username": "ユーザー名",
|
||||
@@ -1933,6 +1963,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "古い記事を最初に",
|
||||
"form.prefs.select.recent_first": "新しい記事を最初に",
|
||||
"form.prefs.label.keyboard_shortcuts": "キーボード・ショートカットを有効にする",
|
||||
"form.prefs.label.entry_swipe": "モバイルのエントリでスワイプジェスチャーを有効にする",
|
||||
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
|
||||
"form.prefs.label.custom_css": "カスタムCSS",
|
||||
"form.import.label.file": "OPML ファイル",
|
||||
@@ -2061,10 +2092,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "Artikel opslaan",
|
||||
"entry.save.completed": "Done!",
|
||||
"entry.save.toast.completed": "Artikel opgeslagen",
|
||||
"entry.scraper.label": "Fetch original content",
|
||||
"entry.scraper.label": "Downloaden",
|
||||
"entry.scraper.title": "Fetch original content",
|
||||
"entry.scraper.completed": "Klaar!",
|
||||
"entry.original.label": "Origineel",
|
||||
"entry.external_link.label": "Externe link",
|
||||
"entry.comments.label": "Comments",
|
||||
"entry.comments.title": "Bekijk de reacties",
|
||||
"entry.share.label": "Deel",
|
||||
@@ -2150,6 +2181,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "Download originele content",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Ster toevoegen/weghalen",
|
||||
"page.keyboard_shortcuts.save_article": "Artikel opslaan",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll artikel naar boven",
|
||||
"page.keyboard_shortcuts.remove_feed": "Verwijder deze feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "Focus instellen op zoekformulier",
|
||||
"page.keyboard_shortcuts.close_modal": "Sluit dialoogscherm",
|
||||
@@ -2243,7 +2275,10 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "Standaard User Agent overschrijven",
|
||||
"form.feed.label.scraper_rules": "Scraper regels",
|
||||
"form.feed.label.rewrite_rules": "Rewrite regels",
|
||||
"form.feed.label.blocklist_rules": "Blokkeer regels",
|
||||
"form.feed.label.keeplist_rules": "toestemmingsregels",
|
||||
"form.feed.label.ignore_http_cache": "Negeer HTTP-cache",
|
||||
"form.feed.label.fetch_via_proxy": "Ophalen via proxy",
|
||||
"form.feed.label.disabled": "Vernieuw deze feed niet",
|
||||
"form.category.label.title": "Naam",
|
||||
"form.user.label.username": "Gebruikersnaam",
|
||||
@@ -2258,6 +2293,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "Oudere items eerst",
|
||||
"form.prefs.select.recent_first": "Recente items eerst",
|
||||
"form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in",
|
||||
"form.prefs.label.entry_swipe": "Schakel veegbewegingen in voor items op mobiel",
|
||||
"form.prefs.label.show_reading_time": "Toon geschatte leestijd voor artikelen",
|
||||
"form.prefs.label.custom_css": "Aangepaste CSS",
|
||||
"form.import.label.file": "OPML-bestand",
|
||||
@@ -2404,10 +2440,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "Zapisz ten artykuł",
|
||||
"entry.save.completed": "Gotowe!",
|
||||
"entry.save.toast.completed": "Artykuł zapisany",
|
||||
"entry.scraper.label": "Pobierz treść",
|
||||
"entry.scraper.label": "Ściągnij",
|
||||
"entry.scraper.title": "Pobierz oryginalną treść",
|
||||
"entry.scraper.completed": "Gotowe!",
|
||||
"entry.original.label": "Oryginalny",
|
||||
"entry.external_link.label": "Link zewnętrzny",
|
||||
"entry.comments.label": "Komentarze",
|
||||
"entry.comments.title": "Zobacz komentarze",
|
||||
"entry.share.label": "Podzielić się",
|
||||
@@ -2494,6 +2530,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "Pobierz oryginalną zawartość",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Dodaj/usuń zakładki",
|
||||
"page.keyboard_shortcuts.save_article": "Zapisz artykuł",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Przewiń artykuł do góry",
|
||||
"page.keyboard_shortcuts.remove_feed": "Usuń ten kanał",
|
||||
"page.keyboard_shortcuts.go_to_search": "Ustaw fokus na formularzu wyszukiwania",
|
||||
"page.keyboard_shortcuts.close_modal": "Zamknij listę skrótów klawiszowych",
|
||||
@@ -2588,7 +2625,10 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "Zastąp domyślny agent użytkownika",
|
||||
"form.feed.label.scraper_rules": "Zasady ekstrakcji",
|
||||
"form.feed.label.rewrite_rules": "Reguły zapisu",
|
||||
"form.feed.label.blocklist_rules": "Zasady blokowania",
|
||||
"form.feed.label.keeplist_rules": "Zasady zezwoleń",
|
||||
"form.feed.label.ignore_http_cache": "Zignoruj pamięć podręczną HTTP",
|
||||
"form.feed.label.fetch_via_proxy": "Pobierz przez proxy",
|
||||
"form.feed.label.disabled": "Не обновлять этот канал",
|
||||
"form.category.label.title": "Tytuł",
|
||||
"form.user.label.username": "Nazwa użytkownika",
|
||||
@@ -2602,6 +2642,7 @@ var translations = map[string]string{
|
||||
"form.prefs.label.entries_per_page": "Wpisy na stronie",
|
||||
"form.prefs.select.older_first": "Najstarsze wpisy jako pierwsze",
|
||||
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe",
|
||||
"form.prefs.label.entry_swipe": "Włącz gest przesuwania na wpisach na telefonie komórkowym",
|
||||
"form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania artykułów",
|
||||
"form.prefs.select.recent_first": "Najnowsze wpisy jako pierwsze",
|
||||
"form.prefs.label.custom_css": "Niestandardowy CSS",
|
||||
@@ -2745,8 +2786,8 @@ var translations = map[string]string{
|
||||
"entry.status.toast.unread": "Marcado como não lido",
|
||||
"entry.status.toast.read": "Marcado como lido",
|
||||
"entry.status.title": "Modificar estado deste item",
|
||||
"entry.bookmark.toggle.on": "Marcar",
|
||||
"entry.bookmark.toggle.off": "Desmarcar",
|
||||
"entry.bookmark.toggle.on": "Favoritar",
|
||||
"entry.bookmark.toggle.off": "Remover dos Favoritos",
|
||||
"entry.bookmark.toast.on": "Favoritado",
|
||||
"entry.bookmark.toast.off": "Desfavoritado",
|
||||
"entry.state.saving": "Salvando...",
|
||||
@@ -2755,10 +2796,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "Salvar esse item",
|
||||
"entry.save.completed": "Feito!",
|
||||
"entry.save.toast.completed": "Item guardado",
|
||||
"entry.scraper.label": "Conteúdo completo",
|
||||
"entry.scraper.label": "Baixar",
|
||||
"entry.scraper.title": "Obter conteúdo completo",
|
||||
"entry.scraper.completed": "Feito!",
|
||||
"entry.original.label": "Original",
|
||||
"entry.external_link.label": "Link externo",
|
||||
"entry.comments.label": "Comentários",
|
||||
"entry.comments.title": "Ver comentários",
|
||||
"entry.share.label": "Compartilhar",
|
||||
@@ -2767,8 +2808,8 @@ var translations = map[string]string{
|
||||
"entry.shared_entry.title": "Abrir link público",
|
||||
"entry.shared_entry.label": "Compartilhar",
|
||||
"entry.estimated_reading_time": [
|
||||
"%d minuto lido",
|
||||
"%d minutos lidos"
|
||||
"Leitura de %d minuto",
|
||||
"Leitura de %d minutos"
|
||||
],
|
||||
"page.shared_entries.title": "Itens compartilhados",
|
||||
"page.unread.title": "Não lídos",
|
||||
@@ -2843,6 +2884,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "Buscar o conteúdo original",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Marcar ou desmarcar como favorito",
|
||||
"page.keyboard_shortcuts.save_article": "Salvar item",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Role o item para cima",
|
||||
"page.keyboard_shortcuts.remove_feed": "Remover essa fonte",
|
||||
"page.keyboard_shortcuts.go_to_search": "Ir para o campo de busca",
|
||||
"page.keyboard_shortcuts.close_modal": "Fechar janela",
|
||||
@@ -2937,8 +2979,11 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "Sobrescrever o agente de usuário (user-agent) padrão",
|
||||
"form.feed.label.scraper_rules": "Regras do scraper",
|
||||
"form.feed.label.rewrite_rules": "Regras para o Rewrite",
|
||||
"form.feed.label.blocklist_rules": "Regras de bloqueio",
|
||||
"form.feed.label.keeplist_rules": "Regras de permissão",
|
||||
"form.feed.label.ignore_http_cache": "Ignorar cache HTTP",
|
||||
"form.feed.label.disabled": "Não atualizar esta fonte",
|
||||
"form.feed.label.fetch_via_proxy": "Buscar via proxy",
|
||||
"form.category.label.title": "Título",
|
||||
"form.user.label.username": "Nome de usuário",
|
||||
"form.user.label.password": "Senha",
|
||||
@@ -2952,6 +2997,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "Itens mais velhos primeiro",
|
||||
"form.prefs.select.recent_first": "Itens mais recentes",
|
||||
"form.prefs.label.keyboard_shortcuts": "Habilitar atalhos do teclado",
|
||||
"form.prefs.label.entry_swipe": "Ativar gesto de deslizar nas entradas no celular",
|
||||
"form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
|
||||
"form.prefs.label.custom_css": "CSS customizado",
|
||||
"form.import.label.file": "Arquivo OPML",
|
||||
@@ -3080,10 +3126,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "Сохранить эту статью",
|
||||
"entry.save.completed": "Готово!",
|
||||
"entry.save.toast.completed": "Статья сохранена",
|
||||
"entry.scraper.label": "Извлечь оригинальное содержимое",
|
||||
"entry.scraper.label": "Скачать",
|
||||
"entry.scraper.title": "Извлечь оригинальное содержимое",
|
||||
"entry.scraper.completed": "Готово!",
|
||||
"entry.original.label": "Оригинал",
|
||||
"entry.external_link.label": "Внешняя ссылка",
|
||||
"entry.comments.label": "Комментарии",
|
||||
"entry.comments.title": "Показать комментарии",
|
||||
"entry.share.label": "Поделиться",
|
||||
@@ -3170,6 +3216,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "Загрузить оригинальное содержимое",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Переключатель избранного",
|
||||
"page.keyboard_shortcuts.save_article": "Сохранить статью",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Прокрутите элемент вверх",
|
||||
"page.keyboard_shortcuts.remove_feed": "Удалить эту подписку",
|
||||
"page.keyboard_shortcuts.go_to_search": "Установить фокус в поисковой форме",
|
||||
"page.keyboard_shortcuts.close_modal": "Закрыть модальный диалог",
|
||||
@@ -3264,7 +3311,10 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "Переопределить User Agent по умолчанию",
|
||||
"form.feed.label.scraper_rules": "Правила Scraper",
|
||||
"form.feed.label.rewrite_rules": "Правила Rewrite",
|
||||
"form.feed.label.blocklist_rules": "Правила блокировки",
|
||||
"form.feed.label.keeplist_rules": "правила разрешений",
|
||||
"form.feed.label.ignore_http_cache": "Игнорировать HTTP-кеш",
|
||||
"form.feed.label.fetch_via_proxy": "Получить через прокси",
|
||||
"form.feed.label.disabled": "Не обновлять этот канал",
|
||||
"form.category.label.title": "Название",
|
||||
"form.user.label.username": "Имя пользователя",
|
||||
@@ -3279,6 +3329,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "Сначала старые записи",
|
||||
"form.prefs.select.recent_first": "Сначала последние записи",
|
||||
"form.prefs.label.keyboard_shortcuts": "Включить сочетания клавиш",
|
||||
"form.prefs.label.entry_swipe": "Включить жест смахивания для записей на мобильном устройстве",
|
||||
"form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
|
||||
"form.prefs.label.custom_css": "Пользовательские CSS",
|
||||
"form.import.label.file": "OPML файл",
|
||||
@@ -3413,10 +3464,10 @@ var translations = map[string]string{
|
||||
"entry.save.title": "保存这篇文章",
|
||||
"entry.save.completed": "完成",
|
||||
"entry.save.toast.completed": "已保存文章",
|
||||
"entry.scraper.label": "抓取原内容",
|
||||
"entry.scraper.label": "下载",
|
||||
"entry.scraper.title": "抓取原内容",
|
||||
"entry.scraper.completed": "完成",
|
||||
"entry.original.label": "原始内容",
|
||||
"entry.external_link.label": "外部链接",
|
||||
"entry.comments.label": "评论",
|
||||
"entry.comments.title": "查看评论",
|
||||
"entry.share.label": "分享",
|
||||
@@ -3499,6 +3550,7 @@ var translations = map[string]string{
|
||||
"page.keyboard_shortcuts.download_content": "下载原始内容",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "切换收藏状态",
|
||||
"page.keyboard_shortcuts.save_article": "保存文章",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "滚动到顶部",
|
||||
"page.keyboard_shortcuts.remove_feed": "删除此Feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "将重点放在搜索表单上",
|
||||
"page.keyboard_shortcuts.close_modal": "关闭模态对话窗口",
|
||||
@@ -3536,7 +3588,7 @@ var translations = map[string]string{
|
||||
"page.sessions.table.current_session": "当前会话",
|
||||
"page.api_keys.title": "API密钥",
|
||||
"page.api_keys.table.description": "描述",
|
||||
"page.api_keys.table.token": "代币",
|
||||
"page.api_keys.table.token": "密钥",
|
||||
"page.api_keys.table.last_used_at": "最后使用",
|
||||
"page.api_keys.table.created_at": "创立日期",
|
||||
"page.api_keys.table.actions": "操作",
|
||||
@@ -3593,7 +3645,10 @@ var translations = map[string]string{
|
||||
"form.feed.label.user_agent": "覆盖默认 User-Agent",
|
||||
"form.feed.label.scraper_rules": "Scraper 规则",
|
||||
"form.feed.label.rewrite_rules": "重写规则",
|
||||
"form.feed.label.blocklist_rules": "封锁规则",
|
||||
"form.feed.label.keeplist_rules": "许可规则",
|
||||
"form.feed.label.ignore_http_cache": "忽略HTTP缓存",
|
||||
"form.feed.label.fetch_via_proxy": "通过代理获取",
|
||||
"form.feed.label.disabled": "请勿刷新此Feed",
|
||||
"form.category.label.title": "标题",
|
||||
"form.user.label.username": "用户名",
|
||||
@@ -3608,6 +3663,7 @@ var translations = map[string]string{
|
||||
"form.prefs.select.older_first": "旧->新",
|
||||
"form.prefs.select.recent_first": "新->旧",
|
||||
"form.prefs.label.keyboard_shortcuts": "启用键盘快捷键",
|
||||
"form.prefs.label.entry_swipe": "在移动设备上的条目上启用滑动手势",
|
||||
"form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
|
||||
"form.prefs.label.custom_css": "自定义CSS",
|
||||
"form.import.label.file": "OPML 文件",
|
||||
@@ -3684,15 +3740,15 @@ var translations = map[string]string{
|
||||
}
|
||||
|
||||
var translationsChecksums = map[string]string{
|
||||
"de_DE": "21e1bfb0f43d71efe38812b4337ddf6980c11ed18f4d06446ff7eda9dfa6b1f1",
|
||||
"en_US": "30cbcb2170782f1e66f69066947bf053f68065d7b270eea879f2c573819dd52b",
|
||||
"es_ES": "50dc7c8c2db7368bae133f5b455721470d314321153d41e4f27436a0f3f176e6",
|
||||
"fr_FR": "373fd2db868961758bd1483c34f117b03aadea17080f268bc8bbd0acdfbc5eed",
|
||||
"it_IT": "8d8f0bd75b4e7dec9370647c888dd9438b691130d9c41f839cdfff8cbc606cb5",
|
||||
"ja_JP": "ec3a21c547e4625ad359624e43ba31b556fb8d8b8ff7fc7a20df089317db99b3",
|
||||
"nl_NL": "20e180be2375f07ec02eb05f372a9102c13037a79e5651ce9bd41507fd2180d2",
|
||||
"pl_PL": "b1526955641823708b4c1ca753b61e1e0561d0a3d33da3f62170540903031b0d",
|
||||
"pt_BR": "cf8e131d39daac82d3157c6538c0643392a06358b7bc98be8579412ebd63f60e",
|
||||
"ru_RU": "4056e4e94861835d44064273371adbbded7190e2b719769886eb99e6c9feaf82",
|
||||
"zh_CN": "044abb0a34eee3d8d5597811d40166762311b8e4cd08b891796113790cc775f0",
|
||||
"de_DE": "66d6feafa5f92c35b10f8c07aa693afe1ea7dca73708cdebfc4cf3edb0478512",
|
||||
"en_US": "fa1771f155b439b46f282069a1628dc95d4170a5d7db14c1b90fa753936e7856",
|
||||
"es_ES": "5b65c97c14e4b1f833ce6047be4b8b29bcb777d7a7a21420d1fb619584746649",
|
||||
"fr_FR": "b58d74f73de2e775cc3f883f2fb19451e306ff32772f2fa8cb3c630d9df395d3",
|
||||
"it_IT": "a488f947b4e0cd8149c4cf7aaa71a8c59976a06559046fbc5b36aa167caaa84c",
|
||||
"ja_JP": "9a3d1484c46be56286f9abf06b596576a3ae44f571d72a40d2eba5bfb02dd921",
|
||||
"nl_NL": "3b91987a9f6640e827c73ca11bb0f9bc58ff79359e792038746e273cd7ae546d",
|
||||
"pl_PL": "ff1dab97559d16331c374c63a91000b0c27f796bd96595e34ca324eb68a7c06e",
|
||||
"pt_BR": "524c0f0dcd81988acd286900481e1a2e0ca9cf789d752e22da8bd5fe31b6acf3",
|
||||
"ru_RU": "805c698b8a053860f737a145acc3616c67c8d80bf1de89bf6bb27fee40e885f2",
|
||||
"zh_CN": "0c6be862c7bd997337696161a1c1d3d2ec5c7adab9c33f29c09d906fcc015a79",
|
||||
}
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "Diesen Artikel speichern",
|
||||
"entry.save.completed": "Erledigt!",
|
||||
"entry.save.toast.completed": "Artikel gespeichert",
|
||||
"entry.scraper.label": "Inhalt herunterladen",
|
||||
"entry.scraper.label": "Herunterladen",
|
||||
"entry.scraper.title": "Inhalt herunterladen",
|
||||
"entry.scraper.completed": "Erledigt!",
|
||||
"entry.original.label": "Original-Artikel",
|
||||
"entry.external_link.label": "Externer Link",
|
||||
"entry.comments.label": "Kommentare",
|
||||
"entry.comments.title": "Kommentare anzeigen",
|
||||
"entry.share.label": "Teilen",
|
||||
@@ -154,6 +154,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "Vollständigen Inhalt herunterladen",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Lesezeichen hinzufügen/entfernen",
|
||||
"page.keyboard_shortcuts.save_article": "Artikel speichern",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Artikel nach oben blättern",
|
||||
"page.keyboard_shortcuts.remove_feed": "Dieses Abonnement entfernen",
|
||||
"page.keyboard_shortcuts.go_to_search": "Fokus auf das Suchformular setzen",
|
||||
"page.keyboard_shortcuts.close_modal": "Liste der Tastenkürzel schließen",
|
||||
@@ -248,7 +249,10 @@
|
||||
"form.feed.label.user_agent": "Standardbenutzeragenten überschreiben",
|
||||
"form.feed.label.scraper_rules": "Extraktionsregeln",
|
||||
"form.feed.label.rewrite_rules": "Umschreiberegeln",
|
||||
"form.feed.label.blocklist_rules": "Regeln blockieren",
|
||||
"form.feed.label.keeplist_rules": "Regeln einhalten",
|
||||
"form.feed.label.ignore_http_cache": "Ignoriere HTTP-cache",
|
||||
"form.feed.label.fetch_via_proxy": "Über Proxy abrufen",
|
||||
"form.feed.label.disabled": "Dieses Abonnement nicht aktualisieren",
|
||||
"form.category.label.title": "Titel",
|
||||
"form.user.label.username": "Benutzername",
|
||||
@@ -263,6 +267,7 @@
|
||||
"form.prefs.select.older_first": "Älteste Artikel zuerst",
|
||||
"form.prefs.select.recent_first": "Neueste Artikel zuerst",
|
||||
"form.prefs.label.keyboard_shortcuts": "Tastaturkürzel aktivieren",
|
||||
"form.prefs.label.entry_swipe": "Wischgeste für Einträge auf dem Handy aktivieren",
|
||||
"form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
|
||||
"form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
|
||||
"form.import.label.file": "OPML Datei",
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "Save this article",
|
||||
"entry.save.completed": "Done!",
|
||||
"entry.save.toast.completed": "Article saved",
|
||||
"entry.scraper.label": "Original",
|
||||
"entry.scraper.label": "Download",
|
||||
"entry.scraper.title": "Fetch original content",
|
||||
"entry.scraper.completed": "Done!",
|
||||
"entry.original.label": "Original",
|
||||
"entry.external_link.label": "External link",
|
||||
"entry.comments.label": "Comments",
|
||||
"entry.comments.title": "View Comments",
|
||||
"entry.share.label": "Share",
|
||||
@@ -154,6 +154,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "Download original content",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Toggle bookmark",
|
||||
"page.keyboard_shortcuts.save_article": "Save article",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll item to top",
|
||||
"page.keyboard_shortcuts.remove_feed": "Remove this feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "Set focus on search form",
|
||||
"page.keyboard_shortcuts.close_modal": "Close modal dialog",
|
||||
@@ -248,7 +249,10 @@
|
||||
"form.feed.label.user_agent": "Override Default User Agent",
|
||||
"form.feed.label.scraper_rules": "Scraper Rules",
|
||||
"form.feed.label.rewrite_rules": "Rewrite Rules",
|
||||
"form.feed.label.blocklist_rules": "Block Rules",
|
||||
"form.feed.label.keeplist_rules": "Keep Rules",
|
||||
"form.feed.label.ignore_http_cache": "Ignore HTTP cache",
|
||||
"form.feed.label.fetch_via_proxy": "Fetch via proxy",
|
||||
"form.feed.label.disabled": "Do not refresh this feed",
|
||||
"form.category.label.title": "Title",
|
||||
"form.user.label.username": "Username",
|
||||
@@ -263,6 +267,7 @@
|
||||
"form.prefs.select.older_first": "Older entries first",
|
||||
"form.prefs.select.recent_first": "Recent entries first",
|
||||
"form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts",
|
||||
"form.prefs.label.entry_swipe": "Enable swipe gesture on entries on mobile",
|
||||
"form.prefs.label.show_reading_time": "Show estimated reading time for articles",
|
||||
"form.prefs.label.custom_css": "Custom CSS",
|
||||
"form.import.label.file": "OPML file",
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "Guardar este articulo",
|
||||
"entry.save.completed": "¡Hecho!",
|
||||
"entry.save.toast.completed": "Artículo guardado",
|
||||
"entry.scraper.label": "Obtener contenido original",
|
||||
"entry.scraper.label": "Descargar",
|
||||
"entry.scraper.title": "Obtener contenido original",
|
||||
"entry.scraper.completed": "¡Hecho!",
|
||||
"entry.original.label": "Original",
|
||||
"entry.external_link.label": "Enlace externo",
|
||||
"entry.comments.label": "Comentarios",
|
||||
"entry.comments.title": "Ver comentarios",
|
||||
"entry.share.label": "Comparta",
|
||||
@@ -154,6 +154,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "Descargar el contento original",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Agregar o quitar marcador",
|
||||
"page.keyboard_shortcuts.save_article": "Guardar artículo",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Desplazar elemento hacia arriba",
|
||||
"page.keyboard_shortcuts.remove_feed": "Quitar esta fuente",
|
||||
"page.keyboard_shortcuts.go_to_search": "Centrarse en el cuadro de búsqueda",
|
||||
"page.keyboard_shortcuts.close_modal": "Cerrar el cuadro de diálogo modal",
|
||||
@@ -248,7 +249,11 @@
|
||||
"form.feed.label.user_agent": "Invalidar el agente de usuario predeterminado",
|
||||
"form.feed.label.scraper_rules": "Reglas de raspador",
|
||||
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
||||
"form.feed.label.blocklist_rules": "Reglas de Filtrado(Bloquear)",
|
||||
"form.feed.label.keeplist_rules": "Reglas de Filtrado(Permitir)",
|
||||
"form.feed.label.blocklist_rules": "Reglas de Blacklist",
|
||||
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
||||
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
||||
"form.feed.label.disabled": "No actualice este feed",
|
||||
"form.category.label.title": "Título",
|
||||
"form.user.label.username": "Nombre de usuario",
|
||||
@@ -263,6 +268,7 @@
|
||||
"form.prefs.select.older_first": "Entradas más viejas primero",
|
||||
"form.prefs.select.recent_first": "Entradas recientes primero",
|
||||
"form.prefs.label.keyboard_shortcuts": "Habilitar atajos de teclado",
|
||||
"form.prefs.label.entry_swipe": "Habilitar el gesto de deslizar el dedo en las entradas en el móvil",
|
||||
"form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
|
||||
"form.prefs.label.custom_css": "CSS personalizado",
|
||||
"form.import.label.file": "Archivo OPML",
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "Sauvegarder cet article",
|
||||
"entry.save.completed": "Terminé !",
|
||||
"entry.save.toast.completed": "Article sauvegardé",
|
||||
"entry.scraper.label": "Original",
|
||||
"entry.scraper.label": "Télécharger",
|
||||
"entry.scraper.title": "Récupérer le contenu original",
|
||||
"entry.scraper.completed": "Terminé !",
|
||||
"entry.original.label": "Original",
|
||||
"entry.external_link.label": "Lien externe",
|
||||
"entry.comments.label": "Commentaires",
|
||||
"entry.comments.title": "Voir les commentaires",
|
||||
"entry.share.label": "Partager",
|
||||
@@ -154,6 +154,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "Télécharger le contenu original",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Ajouter/Enlever favoris",
|
||||
"page.keyboard_shortcuts.save_article": "Sauvegarder l'article",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Faire défiler l'élément vers le haut",
|
||||
"page.keyboard_shortcuts.remove_feed": "Supprimer ce flux",
|
||||
"page.keyboard_shortcuts.go_to_search": "Mettre le focus sur le champ de recherche",
|
||||
"page.keyboard_shortcuts.close_modal": "Fermer la boite de dialogue",
|
||||
@@ -248,7 +249,10 @@
|
||||
"form.feed.label.user_agent": "Remplacer l'agent utilisateur par défaut",
|
||||
"form.feed.label.scraper_rules": "Règles pour récupérer le contenu original",
|
||||
"form.feed.label.rewrite_rules": "Règles de réécriture",
|
||||
"form.feed.label.blocklist_rules": "Règles de blocage",
|
||||
"form.feed.label.keeplist_rules": "Règles d'autorisation",
|
||||
"form.feed.label.ignore_http_cache": "Ignore cache HTTP",
|
||||
"form.feed.label.fetch_via_proxy": "Récupérer via proxy",
|
||||
"form.feed.label.disabled": "Ne pas actualiser ce flux",
|
||||
"form.category.label.title": "Titre",
|
||||
"form.user.label.username": "Nom d'utilisateur",
|
||||
@@ -263,6 +267,7 @@
|
||||
"form.prefs.select.older_first": "Ancien éléments en premier",
|
||||
"form.prefs.select.recent_first": "Éléments récents en premier",
|
||||
"form.prefs.label.keyboard_shortcuts": "Activer les raccourcis clavier",
|
||||
"form.prefs.label.entry_swipe": "Activer le geste de balayage sur les entrées sur mobile",
|
||||
"form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles",
|
||||
"form.prefs.label.custom_css": "CSS personnalisé",
|
||||
"form.import.label.file": "Fichier OPML",
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "Salva questo articolo",
|
||||
"entry.save.completed": "Fatto!",
|
||||
"entry.save.toast.completed": "Articolo salvato",
|
||||
"entry.scraper.label": "Scarica il contenuto integrale",
|
||||
"entry.scraper.label": "Scarica",
|
||||
"entry.scraper.title": "Scarica il contenuto integrale",
|
||||
"entry.scraper.completed": "Fatto!",
|
||||
"entry.original.label": "Originale",
|
||||
"entry.external_link.label": "Link esterno",
|
||||
"entry.comments.label": "Commenti",
|
||||
"entry.comments.title": "Mostra i commenti",
|
||||
"entry.share.label": "Condividi",
|
||||
@@ -154,6 +154,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "Scarica il contenuto integrale",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Aggiungi/rimuovi dai preferiti",
|
||||
"page.keyboard_shortcuts.save_article": "Salva l'articolo",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Scorri l'articolo in alto",
|
||||
"page.keyboard_shortcuts.remove_feed": "Rimuovi questo feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "Apri la casella di ricerca",
|
||||
"page.keyboard_shortcuts.close_modal": "Chiudi la finestra di dialogo",
|
||||
@@ -248,7 +249,10 @@
|
||||
"form.feed.label.user_agent": "Usa user agent personalizzato",
|
||||
"form.feed.label.scraper_rules": "Regole di estrazione del contenuto",
|
||||
"form.feed.label.rewrite_rules": "Regole di impaginazione del contenuto",
|
||||
"form.feed.label.blocklist_rules": "Regole di blocco",
|
||||
"form.feed.label.keeplist_rules": "Regole di autorizzazione",
|
||||
"form.feed.label.ignore_http_cache": "Ignora cache HTTP",
|
||||
"form.feed.label.fetch_via_proxy": "Recuperare tramite proxy",
|
||||
"form.feed.label.disabled": "Non aggiornare questo feed",
|
||||
"form.category.label.title": "Titolo",
|
||||
"form.user.label.username": "Nome utente",
|
||||
@@ -263,6 +267,7 @@
|
||||
"form.prefs.select.older_first": "Prima i più vecchi",
|
||||
"form.prefs.select.recent_first": "Prima i più recenti",
|
||||
"form.prefs.label.keyboard_shortcuts": "Abilita le scorciatoie da tastiera",
|
||||
"form.prefs.label.entry_swipe": "Abilita il gesto di scorrimento sulle voci sul cellulare",
|
||||
"form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
|
||||
"form.prefs.label.custom_css": "CSS personalizzati",
|
||||
"form.import.label.file": "File OPML",
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "この記事を保存",
|
||||
"entry.save.completed": "完了!",
|
||||
"entry.save.toast.completed": "記事は保存されました",
|
||||
"entry.scraper.label": "オリジナルの内容を取得",
|
||||
"entry.scraper.label": "ダウンロード",
|
||||
"entry.scraper.title": "オリジナルの内容を取得",
|
||||
"entry.scraper.completed": "完了!",
|
||||
"entry.original.label": "オリジナル",
|
||||
"entry.external_link.label": "外部リンク",
|
||||
"entry.comments.label": "コメント",
|
||||
"entry.comments.title": "コメントを見る",
|
||||
"entry.share.label": "共有",
|
||||
@@ -154,6 +154,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "オリジナルの内容をダウンロード",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "星を付ける/外す",
|
||||
"page.keyboard_shortcuts.save_article": "記事を保存",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "アイテムを上にスクロール",
|
||||
"page.keyboard_shortcuts.remove_feed": "このフィードを削除",
|
||||
"page.keyboard_shortcuts.go_to_search": "検索フォームにフォーカスを移す",
|
||||
"page.keyboard_shortcuts.close_modal": "モーダルダイアログを閉じる",
|
||||
@@ -248,7 +249,10 @@
|
||||
"form.feed.label.user_agent": "ディフォルトの User Agent を上書きする",
|
||||
"form.feed.label.scraper_rules": "スクラップルール",
|
||||
"form.feed.label.rewrite_rules": "Rewrite ルール",
|
||||
"form.feed.label.blocklist_rules": "ブロックルール",
|
||||
"form.feed.label.keeplist_rules": "許可規則",
|
||||
"form.feed.label.ignore_http_cache": "HTTPキャッシュを無視",
|
||||
"form.feed.label.fetch_via_proxy": "プロキシ経由でフェッチ",
|
||||
"form.feed.label.disabled": "このフィードを更新しない",
|
||||
"form.category.label.title": "タイトル",
|
||||
"form.user.label.username": "ユーザー名",
|
||||
@@ -263,6 +267,7 @@
|
||||
"form.prefs.select.older_first": "古い記事を最初に",
|
||||
"form.prefs.select.recent_first": "新しい記事を最初に",
|
||||
"form.prefs.label.keyboard_shortcuts": "キーボード・ショートカットを有効にする",
|
||||
"form.prefs.label.entry_swipe": "モバイルのエントリでスワイプジェスチャーを有効にする",
|
||||
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
|
||||
"form.prefs.label.custom_css": "カスタムCSS",
|
||||
"form.import.label.file": "OPML ファイル",
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "Artikel opslaan",
|
||||
"entry.save.completed": "Done!",
|
||||
"entry.save.toast.completed": "Artikel opgeslagen",
|
||||
"entry.scraper.label": "Fetch original content",
|
||||
"entry.scraper.label": "Downloaden",
|
||||
"entry.scraper.title": "Fetch original content",
|
||||
"entry.scraper.completed": "Klaar!",
|
||||
"entry.original.label": "Origineel",
|
||||
"entry.external_link.label": "Externe link",
|
||||
"entry.comments.label": "Comments",
|
||||
"entry.comments.title": "Bekijk de reacties",
|
||||
"entry.share.label": "Deel",
|
||||
@@ -155,6 +155,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "Download originele content",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Ster toevoegen/weghalen",
|
||||
"page.keyboard_shortcuts.save_article": "Artikel opslaan",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll artikel naar boven",
|
||||
"page.keyboard_shortcuts.remove_feed": "Verwijder deze feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "Focus instellen op zoekformulier",
|
||||
"page.keyboard_shortcuts.close_modal": "Sluit dialoogscherm",
|
||||
@@ -248,7 +249,10 @@
|
||||
"form.feed.label.user_agent": "Standaard User Agent overschrijven",
|
||||
"form.feed.label.scraper_rules": "Scraper regels",
|
||||
"form.feed.label.rewrite_rules": "Rewrite regels",
|
||||
"form.feed.label.blocklist_rules": "Blokkeer regels",
|
||||
"form.feed.label.keeplist_rules": "toestemmingsregels",
|
||||
"form.feed.label.ignore_http_cache": "Negeer HTTP-cache",
|
||||
"form.feed.label.fetch_via_proxy": "Ophalen via proxy",
|
||||
"form.feed.label.disabled": "Vernieuw deze feed niet",
|
||||
"form.category.label.title": "Naam",
|
||||
"form.user.label.username": "Gebruikersnaam",
|
||||
@@ -263,6 +267,7 @@
|
||||
"form.prefs.select.older_first": "Oudere items eerst",
|
||||
"form.prefs.select.recent_first": "Recente items eerst",
|
||||
"form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in",
|
||||
"form.prefs.label.entry_swipe": "Schakel veegbewegingen in voor items op mobiel",
|
||||
"form.prefs.label.show_reading_time": "Toon geschatte leestijd voor artikelen",
|
||||
"form.prefs.label.custom_css": "Aangepaste CSS",
|
||||
"form.import.label.file": "OPML-bestand",
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "Zapisz ten artykuł",
|
||||
"entry.save.completed": "Gotowe!",
|
||||
"entry.save.toast.completed": "Artykuł zapisany",
|
||||
"entry.scraper.label": "Pobierz treść",
|
||||
"entry.scraper.label": "Ściągnij",
|
||||
"entry.scraper.title": "Pobierz oryginalną treść",
|
||||
"entry.scraper.completed": "Gotowe!",
|
||||
"entry.original.label": "Oryginalny",
|
||||
"entry.external_link.label": "Link zewnętrzny",
|
||||
"entry.comments.label": "Komentarze",
|
||||
"entry.comments.title": "Zobacz komentarze",
|
||||
"entry.share.label": "Podzielić się",
|
||||
@@ -156,6 +156,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "Pobierz oryginalną zawartość",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Dodaj/usuń zakładki",
|
||||
"page.keyboard_shortcuts.save_article": "Zapisz artykuł",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Przewiń artykuł do góry",
|
||||
"page.keyboard_shortcuts.remove_feed": "Usuń ten kanał",
|
||||
"page.keyboard_shortcuts.go_to_search": "Ustaw fokus na formularzu wyszukiwania",
|
||||
"page.keyboard_shortcuts.close_modal": "Zamknij listę skrótów klawiszowych",
|
||||
@@ -250,7 +251,10 @@
|
||||
"form.feed.label.user_agent": "Zastąp domyślny agent użytkownika",
|
||||
"form.feed.label.scraper_rules": "Zasady ekstrakcji",
|
||||
"form.feed.label.rewrite_rules": "Reguły zapisu",
|
||||
"form.feed.label.blocklist_rules": "Zasady blokowania",
|
||||
"form.feed.label.keeplist_rules": "Zasady zezwoleń",
|
||||
"form.feed.label.ignore_http_cache": "Zignoruj pamięć podręczną HTTP",
|
||||
"form.feed.label.fetch_via_proxy": "Pobierz przez proxy",
|
||||
"form.feed.label.disabled": "Не обновлять этот канал",
|
||||
"form.category.label.title": "Tytuł",
|
||||
"form.user.label.username": "Nazwa użytkownika",
|
||||
@@ -264,6 +268,7 @@
|
||||
"form.prefs.label.entries_per_page": "Wpisy na stronie",
|
||||
"form.prefs.select.older_first": "Najstarsze wpisy jako pierwsze",
|
||||
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe",
|
||||
"form.prefs.label.entry_swipe": "Włącz gest przesuwania na wpisach na telefonie komórkowym",
|
||||
"form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania artykułów",
|
||||
"form.prefs.select.recent_first": "Najnowsze wpisy jako pierwsze",
|
||||
"form.prefs.label.custom_css": "Niestandardowy CSS",
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"entry.status.toast.unread": "Marcado como não lido",
|
||||
"entry.status.toast.read": "Marcado como lido",
|
||||
"entry.status.title": "Modificar estado deste item",
|
||||
"entry.bookmark.toggle.on": "Marcar",
|
||||
"entry.bookmark.toggle.off": "Desmarcar",
|
||||
"entry.bookmark.toggle.on": "Favoritar",
|
||||
"entry.bookmark.toggle.off": "Remover dos Favoritos",
|
||||
"entry.bookmark.toast.on": "Favoritado",
|
||||
"entry.bookmark.toast.off": "Desfavoritado",
|
||||
"entry.state.saving": "Salvando...",
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "Salvar esse item",
|
||||
"entry.save.completed": "Feito!",
|
||||
"entry.save.toast.completed": "Item guardado",
|
||||
"entry.scraper.label": "Conteúdo completo",
|
||||
"entry.scraper.label": "Baixar",
|
||||
"entry.scraper.title": "Obter conteúdo completo",
|
||||
"entry.scraper.completed": "Feito!",
|
||||
"entry.original.label": "Original",
|
||||
"entry.external_link.label": "Link externo",
|
||||
"entry.comments.label": "Comentários",
|
||||
"entry.comments.title": "Ver comentários",
|
||||
"entry.share.label": "Compartilhar",
|
||||
@@ -78,8 +78,8 @@
|
||||
"entry.shared_entry.title": "Abrir link público",
|
||||
"entry.shared_entry.label": "Compartilhar",
|
||||
"entry.estimated_reading_time": [
|
||||
"%d minuto lido",
|
||||
"%d minutos lidos"
|
||||
"Leitura de %d minuto",
|
||||
"Leitura de %d minutos"
|
||||
],
|
||||
"page.shared_entries.title": "Itens compartilhados",
|
||||
"page.unread.title": "Não lídos",
|
||||
@@ -154,6 +154,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "Buscar o conteúdo original",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Marcar ou desmarcar como favorito",
|
||||
"page.keyboard_shortcuts.save_article": "Salvar item",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Role o item para cima",
|
||||
"page.keyboard_shortcuts.remove_feed": "Remover essa fonte",
|
||||
"page.keyboard_shortcuts.go_to_search": "Ir para o campo de busca",
|
||||
"page.keyboard_shortcuts.close_modal": "Fechar janela",
|
||||
@@ -248,8 +249,11 @@
|
||||
"form.feed.label.user_agent": "Sobrescrever o agente de usuário (user-agent) padrão",
|
||||
"form.feed.label.scraper_rules": "Regras do scraper",
|
||||
"form.feed.label.rewrite_rules": "Regras para o Rewrite",
|
||||
"form.feed.label.blocklist_rules": "Regras de bloqueio",
|
||||
"form.feed.label.keeplist_rules": "Regras de permissão",
|
||||
"form.feed.label.ignore_http_cache": "Ignorar cache HTTP",
|
||||
"form.feed.label.disabled": "Não atualizar esta fonte",
|
||||
"form.feed.label.fetch_via_proxy": "Buscar via proxy",
|
||||
"form.category.label.title": "Título",
|
||||
"form.user.label.username": "Nome de usuário",
|
||||
"form.user.label.password": "Senha",
|
||||
@@ -263,6 +267,7 @@
|
||||
"form.prefs.select.older_first": "Itens mais velhos primeiro",
|
||||
"form.prefs.select.recent_first": "Itens mais recentes",
|
||||
"form.prefs.label.keyboard_shortcuts": "Habilitar atalhos do teclado",
|
||||
"form.prefs.label.entry_swipe": "Ativar gesto de deslizar nas entradas no celular",
|
||||
"form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
|
||||
"form.prefs.label.custom_css": "CSS customizado",
|
||||
"form.import.label.file": "Arquivo OPML",
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "Сохранить эту статью",
|
||||
"entry.save.completed": "Готово!",
|
||||
"entry.save.toast.completed": "Статья сохранена",
|
||||
"entry.scraper.label": "Извлечь оригинальное содержимое",
|
||||
"entry.scraper.label": "Скачать",
|
||||
"entry.scraper.title": "Извлечь оригинальное содержимое",
|
||||
"entry.scraper.completed": "Готово!",
|
||||
"entry.original.label": "Оригинал",
|
||||
"entry.external_link.label": "Внешняя ссылка",
|
||||
"entry.comments.label": "Комментарии",
|
||||
"entry.comments.title": "Показать комментарии",
|
||||
"entry.share.label": "Поделиться",
|
||||
@@ -156,6 +156,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "Загрузить оригинальное содержимое",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Переключатель избранного",
|
||||
"page.keyboard_shortcuts.save_article": "Сохранить статью",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Прокрутите элемент вверх",
|
||||
"page.keyboard_shortcuts.remove_feed": "Удалить эту подписку",
|
||||
"page.keyboard_shortcuts.go_to_search": "Установить фокус в поисковой форме",
|
||||
"page.keyboard_shortcuts.close_modal": "Закрыть модальный диалог",
|
||||
@@ -250,7 +251,10 @@
|
||||
"form.feed.label.user_agent": "Переопределить User Agent по умолчанию",
|
||||
"form.feed.label.scraper_rules": "Правила Scraper",
|
||||
"form.feed.label.rewrite_rules": "Правила Rewrite",
|
||||
"form.feed.label.blocklist_rules": "Правила блокировки",
|
||||
"form.feed.label.keeplist_rules": "правила разрешений",
|
||||
"form.feed.label.ignore_http_cache": "Игнорировать HTTP-кеш",
|
||||
"form.feed.label.fetch_via_proxy": "Получить через прокси",
|
||||
"form.feed.label.disabled": "Не обновлять этот канал",
|
||||
"form.category.label.title": "Название",
|
||||
"form.user.label.username": "Имя пользователя",
|
||||
@@ -265,6 +269,7 @@
|
||||
"form.prefs.select.older_first": "Сначала старые записи",
|
||||
"form.prefs.select.recent_first": "Сначала последние записи",
|
||||
"form.prefs.label.keyboard_shortcuts": "Включить сочетания клавиш",
|
||||
"form.prefs.label.entry_swipe": "Включить жест смахивания для записей на мобильном устройстве",
|
||||
"form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
|
||||
"form.prefs.label.custom_css": "Пользовательские CSS",
|
||||
"form.import.label.file": "OPML файл",
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"entry.save.title": "保存这篇文章",
|
||||
"entry.save.completed": "完成",
|
||||
"entry.save.toast.completed": "已保存文章",
|
||||
"entry.scraper.label": "抓取原内容",
|
||||
"entry.scraper.label": "下载",
|
||||
"entry.scraper.title": "抓取原内容",
|
||||
"entry.scraper.completed": "完成",
|
||||
"entry.original.label": "原始内容",
|
||||
"entry.external_link.label": "外部链接",
|
||||
"entry.comments.label": "评论",
|
||||
"entry.comments.title": "查看评论",
|
||||
"entry.share.label": "分享",
|
||||
@@ -152,6 +152,7 @@
|
||||
"page.keyboard_shortcuts.download_content": "下载原始内容",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "切换收藏状态",
|
||||
"page.keyboard_shortcuts.save_article": "保存文章",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "滚动到顶部",
|
||||
"page.keyboard_shortcuts.remove_feed": "删除此Feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "将重点放在搜索表单上",
|
||||
"page.keyboard_shortcuts.close_modal": "关闭模态对话窗口",
|
||||
@@ -189,7 +190,7 @@
|
||||
"page.sessions.table.current_session": "当前会话",
|
||||
"page.api_keys.title": "API密钥",
|
||||
"page.api_keys.table.description": "描述",
|
||||
"page.api_keys.table.token": "代币",
|
||||
"page.api_keys.table.token": "密钥",
|
||||
"page.api_keys.table.last_used_at": "最后使用",
|
||||
"page.api_keys.table.created_at": "创立日期",
|
||||
"page.api_keys.table.actions": "操作",
|
||||
@@ -246,7 +247,10 @@
|
||||
"form.feed.label.user_agent": "覆盖默认 User-Agent",
|
||||
"form.feed.label.scraper_rules": "Scraper 规则",
|
||||
"form.feed.label.rewrite_rules": "重写规则",
|
||||
"form.feed.label.blocklist_rules": "封锁规则",
|
||||
"form.feed.label.keeplist_rules": "许可规则",
|
||||
"form.feed.label.ignore_http_cache": "忽略HTTP缓存",
|
||||
"form.feed.label.fetch_via_proxy": "通过代理获取",
|
||||
"form.feed.label.disabled": "请勿刷新此Feed",
|
||||
"form.category.label.title": "标题",
|
||||
"form.user.label.username": "用户名",
|
||||
@@ -261,6 +265,7 @@
|
||||
"form.prefs.select.older_first": "旧->新",
|
||||
"form.prefs.select.recent_first": "新->旧",
|
||||
"form.prefs.label.keyboard_shortcuts": "启用键盘快捷键",
|
||||
"form.prefs.label.entry_swipe": "在移动设备上的条目上启用滑动手势",
|
||||
"form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
|
||||
"form.prefs.label.custom_css": "自定义CSS",
|
||||
"form.import.label.file": "OPML 文件",
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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 metric // import "miniflux.app/metric"
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/storage"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Prometheus Metrics.
|
||||
var (
|
||||
BackgroundFeedRefreshDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "miniflux",
|
||||
Name: "background_feed_refresh_duration",
|
||||
Help: "Processing time to refresh feeds from the background workers",
|
||||
Buckets: prometheus.LinearBuckets(1, 2, 15),
|
||||
},
|
||||
[]string{"status"},
|
||||
)
|
||||
|
||||
ScraperRequestDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "miniflux",
|
||||
Name: "scraper_request_duration",
|
||||
Help: "Web scraper request duration",
|
||||
Buckets: prometheus.LinearBuckets(1, 2, 25),
|
||||
},
|
||||
[]string{"status"},
|
||||
)
|
||||
|
||||
ArchiveEntriesDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "miniflux",
|
||||
Name: "archive_entries_duration",
|
||||
Help: "Archive entries duration",
|
||||
Buckets: prometheus.LinearBuckets(1, 2, 30),
|
||||
},
|
||||
[]string{"status"},
|
||||
)
|
||||
|
||||
usersGauge = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "miniflux",
|
||||
Name: "users",
|
||||
Help: "Number of users",
|
||||
},
|
||||
)
|
||||
|
||||
feedsGauge = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "miniflux",
|
||||
Name: "feeds",
|
||||
Help: "Number of feeds by status",
|
||||
},
|
||||
[]string{"status"},
|
||||
)
|
||||
|
||||
brokenFeedsGauge = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "miniflux",
|
||||
Name: "broken_feeds",
|
||||
Help: "Number of broken feeds",
|
||||
},
|
||||
)
|
||||
|
||||
entriesGauge = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "miniflux",
|
||||
Name: "entries",
|
||||
Help: "Number of entries by status",
|
||||
},
|
||||
[]string{"status"},
|
||||
)
|
||||
)
|
||||
|
||||
// Collector represents a metric collector.
|
||||
type Collector struct {
|
||||
store *storage.Storage
|
||||
refreshInterval int
|
||||
}
|
||||
|
||||
// NewCollector initializes a new metric collector.
|
||||
func NewCollector(store *storage.Storage, refreshInterval int) *Collector {
|
||||
prometheus.MustRegister(BackgroundFeedRefreshDuration)
|
||||
prometheus.MustRegister(ScraperRequestDuration)
|
||||
prometheus.MustRegister(ArchiveEntriesDuration)
|
||||
prometheus.MustRegister(usersGauge)
|
||||
prometheus.MustRegister(feedsGauge)
|
||||
prometheus.MustRegister(brokenFeedsGauge)
|
||||
prometheus.MustRegister(entriesGauge)
|
||||
|
||||
return &Collector{store, refreshInterval}
|
||||
}
|
||||
|
||||
// GatherStorageMetrics polls the database to fetch metrics.
|
||||
func (c *Collector) GatherStorageMetrics() {
|
||||
for range time.Tick(time.Duration(c.refreshInterval) * time.Second) {
|
||||
logger.Debug("[Metric] Collecting database metrics")
|
||||
|
||||
usersGauge.Set(float64(c.store.CountUsers()))
|
||||
brokenFeedsGauge.Set(float64(c.store.CountAllFeedsWithErrors()))
|
||||
|
||||
feedsCount := c.store.CountAllFeeds()
|
||||
for status, count := range feedsCount {
|
||||
feedsGauge.WithLabelValues(status).Set(float64(count))
|
||||
}
|
||||
|
||||
entriesCount := c.store.CountAllEntries()
|
||||
for status, count := range entriesCount {
|
||||
entriesGauge.WithLabelValues(status).Set(float64(count))
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-2
@@ -1,5 +1,5 @@
|
||||
.\" Manpage for miniflux.
|
||||
.TH "MINIFLUX" "1" "June 8, 2019" "\ \&" "\ \&"
|
||||
.TH "MINIFLUX" "1" "September 28, 2020" "\ \&" "\ \&"
|
||||
|
||||
.SH NAME
|
||||
miniflux \- Minimalist and opinionated feed reader
|
||||
@@ -112,6 +112,12 @@ Number of feeds to send to the queue for each interval (default is 10)\&.
|
||||
.TP
|
||||
.B POLLING_SCHEDULER
|
||||
Scheduler used for polling feeds. Possible values are "round_robin" (default) or "entry_frequency"\&.
|
||||
.IP
|
||||
The maximum number of feeds polled for a given period is subject to POLLING_FREQUENCY and BATCH_SIZE\&.
|
||||
.IP
|
||||
When "entry_frequency" is selected, the refresh interval for a given feed is equal to the average updating interval of the last week of the feed\&.
|
||||
.IP
|
||||
The actual number of feeds polled will not exceed the maximum number of feeds that could be polled for a given period\&.
|
||||
.TP
|
||||
.B SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL
|
||||
Maximum interval in minutes for the entry frequency scheduler (default is 24 hours)\&.
|
||||
@@ -147,7 +153,7 @@ Base URL to generate HTML links and base path for cookies\&.
|
||||
Default is http://localhost/\&.
|
||||
.TP
|
||||
.B CLEANUP_FREQUENCY_HOURS
|
||||
Cleanup job frequency, remove old sessions and archive read entries\&.
|
||||
Cleanup job frequency. Remove old sessions and archive entries\&.
|
||||
.br
|
||||
Default is 24 hours\&.
|
||||
.TP
|
||||
@@ -156,6 +162,11 @@ Number of days after marking read items as removed\&.
|
||||
.br
|
||||
Default is 60 days\&.
|
||||
.TP
|
||||
.B CLEANUP_ARCHIVE_UNREAD_DAYS
|
||||
Number of days after marking unread items as removed\&.
|
||||
.br
|
||||
Default is 180 days\&.
|
||||
.TP
|
||||
.B CLEANUP_REMOVE_SESSIONS_DAYS
|
||||
Number of days after removing old sessions from the database\&.
|
||||
.br
|
||||
@@ -185,6 +196,19 @@ Use Let's Encrypt to get automatically a certificate for this domain\&.
|
||||
.B CERT_CACHE
|
||||
Let's Encrypt cache directory (default is /tmp/cert_cache)\&.
|
||||
.TP
|
||||
.B METRICS_COLLECTOR
|
||||
Set to 1 to enable metrics collector. Expose a /metrics endpoint for Prometheus.
|
||||
.br
|
||||
Disabled by default\&.
|
||||
.TP
|
||||
.B METRICS_REFRESH_INTERVAL
|
||||
Refresh interval to collect database metrics\&. Default is 60 seconds\&.
|
||||
.TP
|
||||
.B METRICS_ALLOWED_NETWORKS
|
||||
List of networks allowed to access the metrics endpoint (comma-separated values)\&.
|
||||
.br
|
||||
Default is 127.0.0.1/8\&.
|
||||
.TP
|
||||
.B OAUTH2_PROVIDER
|
||||
OAuth2 provider to use\&. Only google is supported\&.
|
||||
.TP
|
||||
@@ -248,11 +272,27 @@ Maximum body size for HTTP requests in Mebibyte (MiB)\&.
|
||||
.br
|
||||
Default is 15 MiB\&.
|
||||
.TP
|
||||
.B HTTP_CLIENT_PROXY
|
||||
Proxy URL for HTTP client\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B HTTP_CLIENT_USER_AGENT
|
||||
The default User-Agent header to use for the HTTP client. Can be overridden in per-feed settings\&.
|
||||
.br
|
||||
Default is empty. When empty, Miniflux uses a default User-Agent that includes the Miniflux version\&.
|
||||
.TP
|
||||
.B AUTH_PROXY_HEADER
|
||||
Proxy authentication HTTP header\&.
|
||||
.TP
|
||||
.B AUTH_PROXY_USER_CREATION
|
||||
Set to 1 to create users based on proxy authentication information\&.
|
||||
.TP
|
||||
.B MAINTENANCE_MODE
|
||||
Set to 1 to enable maintenance mode\&.
|
||||
.TP
|
||||
.B MAINTENANCE_MESSAGE
|
||||
Define a custom maintenance message\&.
|
||||
|
||||
.SH AUTHORS
|
||||
.P
|
||||
|
||||
+4
-2
@@ -29,10 +29,12 @@ type Entry struct {
|
||||
URL string `json:"url"`
|
||||
CommentsURL string `json:"comments_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 EnclosureList `json:"enclosures,omitempty"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
}
|
||||
@@ -53,11 +55,11 @@ func ValidateEntryStatus(status string) error {
|
||||
// ValidateEntryOrder makes sure the sorting order is valid.
|
||||
func ValidateEntryOrder(order string) error {
|
||||
switch order {
|
||||
case "id", "status", "changed_at", "published_at", "category_title", "category_id":
|
||||
case "id", "status", "changed_at", "published_at", "created_at", "category_title", "category_id":
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf(`Invalid entry order, valid order values are: "id", "status", "changed_at", "published_at", "category_title", "category_id"`)
|
||||
return fmt.Errorf(`Invalid entry order, valid order values are: "id", "status", "changed_at", "published_at", "created_at", "category_title", "category_id"`)
|
||||
}
|
||||
|
||||
// ValidateDirection makes sure the sorting direction is valid.
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ func TestValidateEntryStatus(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateEntryOrder(t *testing.T) {
|
||||
for _, status := range []string{"id", "status", "changed_at", "published_at", "category_title", "category_id"} {
|
||||
for _, status := range []string{"id", "status", "changed_at", "published_at", "created_at", "category_title", "category_id"} {
|
||||
if err := ValidateEntryOrder(status); err != nil {
|
||||
t.Error(`A valid order should not generate any error`)
|
||||
}
|
||||
|
||||
+7
-1
@@ -29,11 +29,14 @@ type Feed struct {
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
Crawler bool `json:"crawler"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Disabled bool `json:"disabled"`
|
||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
Category *Category `json:"category,omitempty"`
|
||||
Entries Entries `json:"entries,omitempty"`
|
||||
Icon *FeedIcon `json:"icon"`
|
||||
@@ -71,13 +74,16 @@ func (f *Feed) WithCategoryID(categoryID int64) {
|
||||
}
|
||||
|
||||
// WithBrowsingParameters defines browsing parameters.
|
||||
func (f *Feed) WithBrowsingParameters(crawler bool, userAgent, username, password, scraperRules, rewriteRules string) {
|
||||
func (f *Feed) WithBrowsingParameters(crawler bool, userAgent, username, password, scraperRules, rewriteRules, blocklistRules, keeplistRules string, fetchViaProxy bool) {
|
||||
f.Crawler = crawler
|
||||
f.UserAgent = userAgent
|
||||
f.Username = username
|
||||
f.Password = password
|
||||
f.ScraperRules = scraperRules
|
||||
f.RewriteRules = rewriteRules
|
||||
f.FetchViaProxy = fetchViaProxy
|
||||
f.BlocklistRules = blocklistRules
|
||||
f.KeeplistRules = keeplistRules
|
||||
}
|
||||
|
||||
// WithError adds a new error message and increment the error counter.
|
||||
|
||||
+25
-3
@@ -48,7 +48,17 @@ func TestFeedCategorySetter(t *testing.T) {
|
||||
|
||||
func TestFeedBrowsingParams(t *testing.T) {
|
||||
feed := &Feed{}
|
||||
feed.WithBrowsingParameters(true, "Custom User Agent", "Username", "Secret", "Some Rule", "Another Rule")
|
||||
feed.WithBrowsingParameters(
|
||||
true,
|
||||
"Custom User Agent",
|
||||
"Username",
|
||||
"Secret",
|
||||
"Scraper Rule",
|
||||
"Rewrite Rule",
|
||||
"Block Rule",
|
||||
"Allow Rule",
|
||||
true,
|
||||
)
|
||||
|
||||
if !feed.Crawler {
|
||||
t.Error(`The crawler must be activated`)
|
||||
@@ -66,13 +76,25 @@ func TestFeedBrowsingParams(t *testing.T) {
|
||||
t.Error(`The password must be set`)
|
||||
}
|
||||
|
||||
if feed.ScraperRules != "Some Rule" {
|
||||
if feed.ScraperRules != "Scraper Rule" {
|
||||
t.Errorf(`The scraper rules must be set`)
|
||||
}
|
||||
|
||||
if feed.RewriteRules != "Another Rule" {
|
||||
if feed.RewriteRules != "Rewrite Rule" {
|
||||
t.Errorf(`The rewrite rules must be set`)
|
||||
}
|
||||
|
||||
if feed.BlocklistRules != "Block Rule" {
|
||||
t.Errorf(`The block list rules must be set`)
|
||||
}
|
||||
|
||||
if feed.KeeplistRules != "Allow Rule" {
|
||||
t.Errorf(`The keep list rules must be set`)
|
||||
}
|
||||
|
||||
if !feed.FetchViaProxy {
|
||||
t.Errorf(`The fetch via proxy is no set`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedErrorCounter(t *testing.T) {
|
||||
|
||||
@@ -16,7 +16,6 @@ type Integration struct {
|
||||
InstapaperPassword string
|
||||
FeverEnabled bool
|
||||
FeverUsername string
|
||||
FeverPassword string
|
||||
FeverToken string
|
||||
WallabagEnabled bool
|
||||
WallabagURL string
|
||||
|
||||
+2
-1
@@ -23,9 +23,10 @@ type User struct {
|
||||
EntryDirection string `json:"entry_sorting_direction"`
|
||||
EntriesPerPage int `json:"entries_per_page"`
|
||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime bool `json:"show_reading_time"`
|
||||
ShowReadingTime bool `json:"show_reading_time"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||
Extra map[string]string `json:"extra"`
|
||||
EntrySwipe bool `json:"entry_swipe"`
|
||||
}
|
||||
|
||||
// NewUser returns a new User.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
ARG BASE_IMAGE_ARCH="amd64"
|
||||
|
||||
FROM ${BASE_IMAGE_ARCH}/golang:buster AS build
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update -q && \
|
||||
apt-get install -y -qq build-essential devscripts dh-make dh-systemd && \
|
||||
mkdir -p /build/debian
|
||||
|
||||
ADD . /src
|
||||
|
||||
CMD ["/src/packaging/debian/build.sh"]
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
|
||||
PKG_ARCH=`dpkg --print-architecture`
|
||||
PKG_DATE=`date -R`
|
||||
PKG_VERSION=`cd /src && git describe --tags --abbrev=0`
|
||||
|
||||
echo "PKG_VERSION=$PKG_VERSION"
|
||||
echo "PKG_ARCH=$PKG_ARCH"
|
||||
echo "PKG_DATE=$PKG_DATE"
|
||||
|
||||
cd /src && \
|
||||
make miniflux && \
|
||||
mkdir -p /build/debian && \
|
||||
cd /build && \
|
||||
cp /src/miniflux /build/ && \
|
||||
cp /src/miniflux.1 /build/ && \
|
||||
cp /src/LICENSE /build/ && \
|
||||
cp /src/packaging/miniflux.conf /build/ && \
|
||||
cp /src/packaging/systemd/miniflux.service /build/debian/ && \
|
||||
cp /src/packaging/debian/compat /build/debian/compat && \
|
||||
cp /src/packaging/debian/copyright /build/debian/copyright && \
|
||||
cp /src/packaging/debian/miniflux.manpages /build/debian/miniflux.manpages && \
|
||||
cp /src/packaging/debian/miniflux.postinst /build/debian/miniflux.postinst && \
|
||||
cp /src/packaging/debian/rules /build/debian/rules && \
|
||||
echo "miniflux ($PKG_VERSION) experimental; urgency=low" > /build/debian/changelog && \
|
||||
echo " * Miniflux version $PKG_VERSION" >> /build/debian/changelog && \
|
||||
echo " -- Frédéric Guillot <f@miniflux.net> $PKG_DATE" >> /build/debian/changelog && \
|
||||
sed "s/__PKG_ARCH__/${PKG_ARCH}/g" /src/packaging/debian/control > /build/debian/control && \
|
||||
dpkg-buildpackage -us -uc -b && \
|
||||
lintian --check --color always ../*.deb && \
|
||||
cp ../*.deb /pkg/
|
||||
@@ -0,0 +1 @@
|
||||
9
|
||||
@@ -0,0 +1,12 @@
|
||||
Source: miniflux
|
||||
Maintainer: Frédéric Guillot <f@miniflux.net>
|
||||
Build-Depends: debhelper (>= 9), dh-systemd
|
||||
|
||||
Package: miniflux
|
||||
Architecture: __PKG_ARCH__
|
||||
Section: web
|
||||
Priority: optional
|
||||
Description: Minimalist Feed Reader
|
||||
Miniflux is a minimalist and opinionated feed reader
|
||||
Homepage: https://miniflux.app
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}, adduser
|
||||
@@ -0,0 +1,3 @@
|
||||
Files: *
|
||||
Copyright: 2017-2020 Frédéric Guillot
|
||||
License: Apache
|
||||
@@ -0,0 +1 @@
|
||||
miniflux.1
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
adduser --system --disabled-password --disabled-login --home /var/empty \
|
||||
--no-create-home --quiet --force-badname --group miniflux
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
DESTDIR=debian/miniflux
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd
|
||||
|
||||
override_dh_auto_clean:
|
||||
override_dh_auto_test:
|
||||
override_dh_auto_build:
|
||||
override_dh_auto_install:
|
||||
mkdir -p $(DESTDIR)/etc
|
||||
mkdir -p $(DESTDIR)/usr/bin
|
||||
cp miniflux.conf $(DESTDIR)/etc/miniflux.conf
|
||||
cp miniflux $(DESTDIR)/usr/bin/miniflux
|
||||
|
||||
override_dh_installinit:
|
||||
dh_installinit --noscripts
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM golang:alpine AS build
|
||||
RUN apk add --no-cache --update git
|
||||
ADD . /go/src/app
|
||||
WORKDIR /go/src/app
|
||||
RUN go generate
|
||||
RUN go build \
|
||||
-o miniflux \
|
||||
-ldflags="-s -w -X 'miniflux.app/version.Version=`git describe --tags --abbrev=0`' -X 'miniflux.app/version.Commit=`git rev-parse --short HEAD`' -X 'miniflux.app/version.BuildDate=`date +%FT%T%z`'" \
|
||||
main.go
|
||||
|
||||
FROM alpine:latest
|
||||
EXPOSE 8080
|
||||
ENV LISTEN_ADDR 0.0.0.0:8080
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
COPY --from=build /go/src/app/miniflux /usr/bin/miniflux
|
||||
USER nobody
|
||||
CMD ["/usr/bin/miniflux"]
|
||||
@@ -0,0 +1,3 @@
|
||||
# See https://miniflux.app/docs/configuration.html
|
||||
|
||||
RUN_MIGRATIONS=1
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM golang:1 AS build
|
||||
ADD . /go/src/app
|
||||
WORKDIR /go/src/app
|
||||
RUN go generate
|
||||
RUN go build \
|
||||
-o miniflux \
|
||||
-ldflags="-s -w -X 'miniflux.app/version.Version=`git describe --tags --abbrev=0`' -X 'miniflux.app/version.Commit=`git rev-parse --short HEAD`' -X 'miniflux.app/version.BuildDate=`date +%FT%T%z`'" \
|
||||
main.go
|
||||
|
||||
FROM centos:latest
|
||||
RUN dnf install -y rpm-build
|
||||
RUN mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||
RUN echo "%_topdir /root/rpmbuild" >> .rpmmacros
|
||||
COPY --from=build /go/src/app/miniflux /root/rpmbuild/SOURCES/miniflux
|
||||
COPY --from=build /go/src/app/LICENSE /root/rpmbuild/SOURCES/
|
||||
COPY --from=build /go/src/app/ChangeLog /root/rpmbuild/SOURCES/
|
||||
COPY --from=build /go/src/app/miniflux.1 /root/rpmbuild/SOURCES/
|
||||
COPY --from=build /go/src/app/packaging/systemd/miniflux.service /root/rpmbuild/SOURCES/
|
||||
COPY --from=build /go/src/app/packaging/miniflux.conf /root/rpmbuild/SOURCES/
|
||||
COPY --from=build /go/src/app/packaging/rpm/miniflux.spec /root/rpmbuild/SPECS/miniflux.spec
|
||||
@@ -0,0 +1,61 @@
|
||||
%undefine _disable_source_fetch
|
||||
|
||||
Name: miniflux
|
||||
Version: %{_miniflux_version}
|
||||
Release: 1.0
|
||||
Summary: Minimalist and opinionated feed reader
|
||||
URL: https://miniflux.app/
|
||||
License: ASL 2.0
|
||||
Source0: miniflux
|
||||
Source1: miniflux.service
|
||||
Source2: miniflux.conf
|
||||
Source3: miniflux.1
|
||||
Source4: LICENSE
|
||||
Source5: ChangeLog
|
||||
BuildRoot: %{_topdir}/BUILD/%{name}-%{version}-%{release}
|
||||
BuildArch: x86_64
|
||||
Requires(pre): shadow-utils
|
||||
|
||||
%{?systemd_requires}
|
||||
BuildRequires: systemd
|
||||
|
||||
%description
|
||||
%{summary}
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}%{_bindir}
|
||||
install -p -m 755 %{SOURCE0} %{buildroot}%{_bindir}/miniflux
|
||||
install -D -m 644 %{SOURCE1} %{buildroot}%{_unitdir}/miniflux.service
|
||||
install -D -m 600 %{SOURCE2} %{buildroot}%{_sysconfdir}/miniflux.conf
|
||||
install -D -m 644 %{SOURCE3} %{buildroot}%{_mandir}/man1/miniflux.1
|
||||
install -D -m 644 %{SOURCE4} %{buildroot}%{_docdir}/miniflux/LICENSE
|
||||
install -D -m 644 %{SOURCE5} %{buildroot}%{_docdir}/miniflux/ChangeLog
|
||||
|
||||
%files
|
||||
%defattr(755,root,root)
|
||||
%{_bindir}/miniflux
|
||||
%{_docdir}/miniflux
|
||||
%defattr(644,root,root)
|
||||
%{_unitdir}/miniflux.service
|
||||
%{_mandir}/man1/miniflux.1*
|
||||
%{_docdir}/miniflux/*
|
||||
%defattr(600,root,root)
|
||||
%config(noreplace) %{_sysconfdir}/miniflux.conf
|
||||
|
||||
%pre
|
||||
getent group miniflux >/dev/null || groupadd -r miniflux
|
||||
getent passwd miniflux >/dev/null || \
|
||||
useradd -r -g miniflux -d /dev/null -s /sbin/nologin \
|
||||
-c "Miniflux Daemon" miniflux
|
||||
exit 0
|
||||
|
||||
%post
|
||||
%systemd_post miniflux.service
|
||||
|
||||
%preun
|
||||
%systemd_preun miniflux.service
|
||||
|
||||
%postun
|
||||
%systemd_postun_with_restart miniflux.service
|
||||
|
||||
%changelog
|
||||
@@ -0,0 +1,47 @@
|
||||
# Changing the systemd config can be done like this:
|
||||
# 1) Edit the config file: vim /usr/lib/systemd/system/miniflux.service
|
||||
# 2) Reload systemd: systemctl daemon-reload
|
||||
# 3) Restart the process: systemctl restart miniflux
|
||||
|
||||
[Unit]
|
||||
Description=Miniflux Feed Reader
|
||||
After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/miniflux.conf
|
||||
User=miniflux
|
||||
ExecStart=/usr/bin/miniflux
|
||||
Restart=always
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#NoNewPrivileges=
|
||||
NoNewPrivileges=true
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateDevices=
|
||||
PrivateDevices=true
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectControlGroups=
|
||||
ProtectControlGroups=true
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome=
|
||||
ProtectHome=true
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelModules=
|
||||
ProtectKernelModules=true
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelTunables=
|
||||
ProtectKernelTunables=true
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectSystem=
|
||||
ProtectSystem=strict
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictRealtime=
|
||||
RestrictRealtime=true
|
||||
|
||||
# Keep at least the /run folder writeable if Miniflux is configured to use a Unix socket.
|
||||
# For example, the socket could be LISTEN_ADDR=/run/miniflux/miniflux.sock
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ReadWritePaths=
|
||||
ReadWritePaths=/run
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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 proxy // import "miniflux.app/proxy"
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/url"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var regexSplitSrcset = regexp.MustCompile(`,\s+`)
|
||||
|
||||
// ImageProxyRewriter replaces image URLs with internal proxy URLs.
|
||||
func ImageProxyRewriter(router *mux.Router, data string) string {
|
||||
proxyImages := config.Opts.ProxyImages()
|
||||
if proxyImages == "none" {
|
||||
return data
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
doc.Find("img").Each(func(i int, img *goquery.Selection) {
|
||||
if srcAttr, ok := img.Attr("src"); ok {
|
||||
if !isDataURL(srcAttr) && (proxyImages == "all" || !url.IsHTTPS(srcAttr)) {
|
||||
img.SetAttr("src", ProxifyURL(router, srcAttr))
|
||||
}
|
||||
}
|
||||
|
||||
if srcsetAttr, ok := img.Attr("srcset"); ok {
|
||||
if proxyImages == "all" || !url.IsHTTPS(srcsetAttr) {
|
||||
proxifySourceSet(img, router, srcsetAttr)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
doc.Find("picture source").Each(func(i int, sourceElement *goquery.Selection) {
|
||||
if srcsetAttr, ok := sourceElement.Attr("srcset"); ok {
|
||||
if proxyImages == "all" || !url.IsHTTPS(srcsetAttr) {
|
||||
proxifySourceSet(sourceElement, router, srcsetAttr)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
output, err := doc.Find("body").First().Html()
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func proxifySourceSet(element *goquery.Selection, router *mux.Router, attributeValue string) {
|
||||
var proxifiedSources []string
|
||||
|
||||
for _, source := range regexSplitSrcset.Split(attributeValue, -1) {
|
||||
parts := strings.Split(strings.TrimSpace(source), " ")
|
||||
nbParts := len(parts)
|
||||
|
||||
if nbParts > 0 {
|
||||
rewrittenSource := parts[0]
|
||||
if !isDataURL(rewrittenSource) {
|
||||
rewrittenSource = ProxifyURL(router, rewrittenSource)
|
||||
}
|
||||
|
||||
if nbParts > 1 {
|
||||
rewrittenSource += " " + parts[1]
|
||||
}
|
||||
|
||||
proxifiedSources = append(proxifiedSources, rewrittenSource)
|
||||
}
|
||||
}
|
||||
|
||||
if len(proxifiedSources) > 0 {
|
||||
element.SetAttr("srcset", strings.Join(proxifiedSources, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func isDataURL(s string) bool {
|
||||
return strings.HasPrefix(s, "data:")
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
// 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 proxy // import "miniflux.app/proxy"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"miniflux.app/config"
|
||||
)
|
||||
|
||||
func TestProxyFilterWithHttpDefault(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "http-only")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithHttpsDefault(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "http-only")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithHttpNever(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "none")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
expected := input
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithHttpsNever(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "none")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
expected := input
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithHttpAlways(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "all")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithHttpsAlways(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "all")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
expected := `<p><img src="/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithHttpInvalid(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "invalid")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithHttpsInvalid(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "invalid")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithSrcset(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "all")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w" alt="test"></p>`
|
||||
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w" alt="test"/></p>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithPictureSource(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "all")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<picture><source srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w"></picture>`
|
||||
expected := `<picture><source srcset="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w"/></picture>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageProxyWithImageDataURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "all")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<img src="data:image/gif;base64,test">`
|
||||
expected := `<img src="data:image/gif;base64,test"/>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageProxyWithImageSourceDataURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "all")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
|
||||
expected := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
|
||||
output := ImageProxyRewriter(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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 proxy // import "miniflux.app/proxy"
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"miniflux.app/http/route"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// ProxifyURL generates an URL for a proxified resource.
|
||||
func ProxifyURL(router *mux.Router, link string) string {
|
||||
return route.Path(router, "proxy", "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
|
||||
}
|
||||
+17
-5
@@ -27,12 +27,24 @@ type atom03Feed struct {
|
||||
Entries []atom03Entry `xml:"entry"`
|
||||
}
|
||||
|
||||
func (a *atom03Feed) Transform() *model.Feed {
|
||||
feed := new(model.Feed)
|
||||
feed.FeedURL = a.Links.firstLinkWithRelation("self")
|
||||
feed.SiteURL = a.Links.originalLink()
|
||||
feed.Title = a.Title.String()
|
||||
func (a *atom03Feed) Transform(baseURL string) *model.Feed {
|
||||
var err error
|
||||
|
||||
feed := new(model.Feed)
|
||||
|
||||
feedURL := a.Links.firstLinkWithRelation("self")
|
||||
feed.FeedURL, err = url.AbsoluteURL(baseURL, feedURL)
|
||||
if err != nil {
|
||||
feed.FeedURL = feedURL
|
||||
}
|
||||
|
||||
siteURL := a.Links.originalLink()
|
||||
feed.SiteURL, err = url.AbsoluteURL(baseURL, siteURL)
|
||||
if err != nil {
|
||||
feed.SiteURL = siteURL
|
||||
}
|
||||
|
||||
feed.Title = a.Title.String()
|
||||
if feed.Title == "" {
|
||||
feed.Title = feed.SiteURL
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestParseAtom03(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("http://diveintomark.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func TestParseAtom03(t *testing.T) {
|
||||
t.Errorf("Incorrect title, got: %s", feed.Title)
|
||||
}
|
||||
|
||||
if feed.FeedURL != "" {
|
||||
if feed.FeedURL != "http://diveintomark.org/" {
|
||||
t.Errorf("Incorrect feed URL, got: %s", feed.FeedURL)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func TestParseAtom03WithoutFeedTitle(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("http://diveintomark.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func TestParseAtom03WithoutEntryTitle(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("http://diveintomark.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func TestParseAtom03WithSummaryOnly(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("http://diveintomark.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func TestParseAtom03WithXMLContent(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("http://diveintomark.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func TestParseAtom03WithBase64Content(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("http://diveintomark.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+19
-10
@@ -6,7 +6,6 @@ package atom // import "miniflux.app/reader/atom"
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"html"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -32,12 +31,24 @@ type atom10Feed struct {
|
||||
Entries []atom10Entry `xml:"entry"`
|
||||
}
|
||||
|
||||
func (a *atom10Feed) Transform() *model.Feed {
|
||||
feed := new(model.Feed)
|
||||
feed.FeedURL = a.Links.firstLinkWithRelation("self")
|
||||
feed.SiteURL = a.Links.originalLink()
|
||||
feed.Title = a.Title.String()
|
||||
func (a *atom10Feed) Transform(baseURL string) *model.Feed {
|
||||
var err error
|
||||
|
||||
feed := new(model.Feed)
|
||||
|
||||
feedURL := a.Links.firstLinkWithRelation("self")
|
||||
feed.FeedURL, err = url.AbsoluteURL(baseURL, feedURL)
|
||||
if err != nil {
|
||||
feed.FeedURL = feedURL
|
||||
}
|
||||
|
||||
siteURL := a.Links.originalLink()
|
||||
feed.SiteURL, err = url.AbsoluteURL(baseURL, siteURL)
|
||||
if err != nil {
|
||||
feed.SiteURL = siteURL
|
||||
}
|
||||
|
||||
feed.Title = a.Title.String()
|
||||
if feed.Title == "" {
|
||||
feed.Title = feed.SiteURL
|
||||
}
|
||||
@@ -124,7 +135,7 @@ func (a *atom10Entry) entryDate() time.Time {
|
||||
if dateText != "" {
|
||||
result, err := date.Parse(dateText)
|
||||
if err != nil {
|
||||
logger.Error("atom: %v", err)
|
||||
logger.Error("atom: %v (entry ID = %s)", err, a.ID)
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
@@ -221,10 +232,8 @@ func (a *atom10Text) String() string {
|
||||
switch {
|
||||
case a.Type == "xhtml":
|
||||
content = a.XML
|
||||
case a.Type == "html":
|
||||
default:
|
||||
content = a.Data
|
||||
case a.Type == "text" || a.Type == "":
|
||||
content = html.EscapeString(a.Data)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(content)
|
||||
|
||||
+122
-29
@@ -32,7 +32,7 @@ func TestParseAtomSample(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("http://example.org/feed.xml", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func TestParseAtomSample(t *testing.T) {
|
||||
t.Errorf("Incorrect title, got: %s", feed.Title)
|
||||
}
|
||||
|
||||
if feed.FeedURL != "" {
|
||||
if feed.FeedURL != "http://example.org/feed.xml" {
|
||||
t.Errorf("Incorrect feed URL, got: %s", feed.FeedURL)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestParseFeedWithoutTitle(t *testing.T) {
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func TestParseEntryWithoutTitle(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func TestParseFeedURL(t *testing.T) {
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -154,6 +154,42 @@ func TestParseFeedURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFeedWithRelativeURL(t *testing.T) {
|
||||
data := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Example Feed</title>
|
||||
<link href="/blog/atom.xml" rel="self" type="application/atom+xml"/>
|
||||
<link href="/blog"/>
|
||||
|
||||
<entry>
|
||||
<title>Test</title>
|
||||
<link href="/blog/article.html"/>
|
||||
<link href="/blog/article.html" rel="alternate" type="text/html"/>
|
||||
<id>/blog/article.html</id>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
<summary>Some text.</summary>
|
||||
</entry>
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if feed.FeedURL != "https://example.org/blog/atom.xml" {
|
||||
t.Errorf("Incorrect feed URL, got: %s", feed.FeedURL)
|
||||
}
|
||||
|
||||
if feed.SiteURL != "https://example.org/blog" {
|
||||
t.Errorf("Incorrect site URL, got: %s", feed.SiteURL)
|
||||
}
|
||||
|
||||
if feed.Entries[0].URL != "https://example.org/blog/article.html" {
|
||||
t.Errorf("Incorrect entry URL, got: %s", feed.Entries[0].URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEntryWithRelativeURL(t *testing.T) {
|
||||
data := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
@@ -170,7 +206,7 @@ func TestParseEntryWithRelativeURL(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.net/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -198,7 +234,7 @@ func TestParseEntryTitleWithWhitespaces(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -224,7 +260,7 @@ func TestParseEntryTitleWithHTMLAndCDATA(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -250,7 +286,7 @@ func TestParseEntryTitleWithHTML(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -276,7 +312,7 @@ func TestParseEntryTitleWithXHTML(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -302,7 +338,7 @@ func TestParseEntrySummaryWithXHTML(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -328,7 +364,7 @@ func TestParseEntrySummaryWithHTML(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -354,12 +390,12 @@ func TestParseEntrySummaryWithPlainText(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if feed.Entries[0].Content != "<Some text.>" {
|
||||
if feed.Entries[0].Content != "<Some text.>" {
|
||||
t.Errorf("Incorrect entry content, got: %s", feed.Entries[0].Content)
|
||||
}
|
||||
}
|
||||
@@ -383,7 +419,7 @@ func TestParseEntryWithAuthorName(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -412,7 +448,7 @@ func TestParseEntryWithoutAuthorName(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -462,7 +498,7 @@ func TestParseEntryWithEnclosures(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -522,7 +558,7 @@ func TestParseEntryWithoutEnclosureURL(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -555,7 +591,7 @@ func TestParseEntryWithPublished(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -581,7 +617,7 @@ func TestParseEntryWithPublishedAndUpdated(t *testing.T) {
|
||||
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -593,12 +629,69 @@ func TestParseEntryWithPublishedAndUpdated(t *testing.T) {
|
||||
|
||||
func TestParseInvalidXml(t *testing.T) {
|
||||
data := `garbage`
|
||||
_, err := Parse(bytes.NewBufferString(data))
|
||||
_, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err == nil {
|
||||
t.Error("Parse should returns an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTitleWithSingleQuote(t *testing.T) {
|
||||
data := `
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>' or ’</title>
|
||||
<link href="http://example.org/"/>
|
||||
</feed>
|
||||
`
|
||||
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if feed.Title != "' or ’" {
|
||||
t.Errorf(`Incorrect title, got: %q`, feed.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTitleWithEncodedSingleQuote(t *testing.T) {
|
||||
data := `
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title type="html">Test's Blog</title>
|
||||
<link href="http://example.org/"/>
|
||||
</feed>
|
||||
`
|
||||
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if feed.Title != "Test's Blog" {
|
||||
t.Errorf(`Incorrect title, got: %q`, feed.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTitleWithSingleQuoteAndHTMLType(t *testing.T) {
|
||||
data := `
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title type="html">O’Hara</title>
|
||||
<link href="http://example.org/"/>
|
||||
</feed>
|
||||
`
|
||||
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if feed.Title != "O’Hara" {
|
||||
t.Errorf(`Incorrect title, got: %q`, feed.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWithHTMLEntity(t *testing.T) {
|
||||
data := `
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
@@ -608,7 +701,7 @@ func TestParseWithHTMLEntity(t *testing.T) {
|
||||
</feed>
|
||||
`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -627,7 +720,7 @@ func TestParseWithInvalidCharacterEntity(t *testing.T) {
|
||||
</feed>
|
||||
`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -660,7 +753,7 @@ A website: http://example.org/</media:description>
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -726,7 +819,7 @@ A website: http://example.org/</media:description>
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -797,7 +890,7 @@ func TestParseRepliesLinkRelationWithHTMLType(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -841,7 +934,7 @@ func TestParseRepliesLinkRelationWithXHTMLType(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -880,7 +973,7 @@ func TestParseRepliesLinkRelationWithNoType(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -920,7 +1013,7 @@ func TestAbsoluteCommentsURL(t *testing.T) {
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
feed, err := Parse(bytes.NewBufferString(data))
|
||||
feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
)
|
||||
|
||||
type atomFeed interface {
|
||||
Transform() *model.Feed
|
||||
Transform(baseURL string) *model.Feed
|
||||
}
|
||||
|
||||
// Parse returns a normalized feed struct from a Atom feed.
|
||||
func Parse(r io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
func Parse(baseURL string, r io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
var buf bytes.Buffer
|
||||
tee := io.TeeReader(r, &buf)
|
||||
|
||||
@@ -36,7 +36,7 @@ func Parse(r io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
return nil, errors.NewLocalizedError("Unable to parse Atom feed: %q", err)
|
||||
}
|
||||
|
||||
return rawFeed.Transform(), nil
|
||||
return rawFeed.Transform(baseURL), nil
|
||||
}
|
||||
|
||||
func getAtomFeedVersion(data io.Reader) string {
|
||||
|
||||
+109
-44
@@ -23,6 +23,8 @@ var dateFormats = []string{
|
||||
time.RFC1123Z,
|
||||
time.RFC1123,
|
||||
time.ANSIC,
|
||||
"Mon, 02 Jan 2006 15:04:05 MST -07:00",
|
||||
"Mon, January 2, 2006, 3:04 PM MST",
|
||||
"Mon, January 2 2006 15:04:05 -0700",
|
||||
"Mon, January 02, 2006, 15:04:05 MST",
|
||||
"Mon, January 02, 2006 15:04:05 MST",
|
||||
@@ -37,6 +39,9 @@ var dateFormats = []string{
|
||||
"Mon Jan 02, 2006 3:04 pm",
|
||||
"Mon, Jan 02,2006 15:04:05 MST",
|
||||
"Mon Jan 02 2006 15:04:05 -0700",
|
||||
"Mon, 02/01/2006",
|
||||
"Monday, 2. January 2006 - 15:04",
|
||||
"Monday 02 January 2006",
|
||||
"Monday, January 2, 2006 15:04:05 MST",
|
||||
"Monday, January 2, 2006 03:04 PM",
|
||||
"Monday, January 2, 2006",
|
||||
@@ -111,6 +116,11 @@ var dateFormats = []string{
|
||||
"Mon, 02 Jan 2006",
|
||||
"Mon, 02 Jan 06 15:04:05 MST",
|
||||
"Mon, 02 Jan 2006 3:04 PM MST",
|
||||
"Mon Jan 02 2006 15:04:05 MST",
|
||||
"Mon, 01 02 2006 15:04:05 -0700",
|
||||
"Mon, 2th Jan 2006 15:05:05 MST",
|
||||
"Jan. 2, 2006, 3:04 a.m.",
|
||||
"fri, 02 jan 2006 15:04:05 -0700",
|
||||
"January 02 2006 03:04:05 PM",
|
||||
"January 2, 2006 3:04 PM",
|
||||
"January 2, 2006, 3:04 p.m.",
|
||||
@@ -145,6 +155,7 @@ var dateFormats = []string{
|
||||
"2006-1-2T15:04:05Z",
|
||||
"2006-1-2 15:04:05",
|
||||
"2006-1-2",
|
||||
"2006-01-02T15:04:05-07:00Z",
|
||||
"2006-1-02T15:04:05Z",
|
||||
"2006-01-02T15:04Z",
|
||||
"2006-01-02T15:04-07:00",
|
||||
@@ -156,6 +167,7 @@ var dateFormats = []string{
|
||||
"2006-01-02T15:04:05 -0700",
|
||||
"2006-01-02T15:04:05:00",
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02T15:04",
|
||||
"2006-01-02 at 15:04:05",
|
||||
"2006-01-02 15:04:05Z",
|
||||
"2006-01-02 15:04:05 MST",
|
||||
@@ -195,41 +207,123 @@ var dateFormats = []string{
|
||||
"01/02/2006 - 15:04",
|
||||
"01/02/2006",
|
||||
"01-02-2006",
|
||||
"Jan. 2006",
|
||||
"Jan. 2, 2006, 03:04 p.m.",
|
||||
"2006-01-02 15:04:05 -07:00",
|
||||
"2 January, 2006",
|
||||
}
|
||||
|
||||
var invalidTimezoneReplacer = strings.NewReplacer(
|
||||
"Europe/Brussels", "CET",
|
||||
"GMT+0000 (Coordinated Universal Time)", "GMT",
|
||||
"GMT-", "GMT -",
|
||||
)
|
||||
|
||||
var invalidLocalizedDateReplacer = strings.NewReplacer(
|
||||
"Mo,", "Mon,",
|
||||
"Di,", "Tue,",
|
||||
"Mi,", "Wed,",
|
||||
"Do,", "Thu,",
|
||||
"Fr,", "Fri,",
|
||||
"Sa,", "Sat,",
|
||||
"So,", "Sun,",
|
||||
"Mär ", "Mar ",
|
||||
"Mai ", "May ",
|
||||
"Okt ", "Oct ",
|
||||
"Dez ", "Dec ",
|
||||
"lun,", "Mon,",
|
||||
"mar,", "Tue,",
|
||||
"mer,", "Wed,",
|
||||
"jeu,", "Thu,",
|
||||
"ven,", "Fri,",
|
||||
"sam,", "Sat,",
|
||||
"dim,", "Sun,",
|
||||
"lun.", "Mon",
|
||||
"mar.", "Tue",
|
||||
"mer.", "Wed",
|
||||
"jeu.", "Thu",
|
||||
"ven.", "Fri",
|
||||
"sam.", "Sat",
|
||||
"dim.", "Sun",
|
||||
"Lundi,", "Monday,",
|
||||
"Mardi,", "Tuesday,",
|
||||
"Mercredi,", "Wednesday,",
|
||||
"Jeudi,", "Thursday,",
|
||||
"Vendredi,", "Friday,",
|
||||
"Samedi,", "Saturday,",
|
||||
"Dimanche,", "Sunday,",
|
||||
"jan.", "January ",
|
||||
"feb.", "February ",
|
||||
"mars.", "March ",
|
||||
"avril.", "April ",
|
||||
"mai.", "May ",
|
||||
"juin.", "June ",
|
||||
"juil.", "July",
|
||||
"août.", "August",
|
||||
"sept.", "September",
|
||||
"oct.", "October",
|
||||
"nov.", "November",
|
||||
"dec.", "December",
|
||||
"déc.", "December",
|
||||
"janvier ", "January ",
|
||||
"février ", "February ",
|
||||
"mars ", "March ",
|
||||
"avril ", "April ",
|
||||
"mai ", "May ",
|
||||
"juin ", "June ",
|
||||
"juillet ", "July",
|
||||
"août ", "August",
|
||||
"septembre ", "September",
|
||||
"octobre ", "October",
|
||||
"november ", "November",
|
||||
"décembre ", "December",
|
||||
"Janvier", "January",
|
||||
"Février", "February",
|
||||
"Mars", "March",
|
||||
"Avril", "April",
|
||||
"Mai", "May",
|
||||
"Juin", "June",
|
||||
"Juillet", "July",
|
||||
"Août", "August",
|
||||
"Septembre", "September",
|
||||
"Octobre", "October",
|
||||
"Novembre", "November",
|
||||
"Décembre", "December",
|
||||
"avr ", "Apr ",
|
||||
"mai ", "May ",
|
||||
"jui ", "Jun ",
|
||||
"juin ", "June ",
|
||||
)
|
||||
|
||||
// Parse parses a given date string using a large
|
||||
// list of commonly found feed date formats.
|
||||
func Parse(ds string) (t time.Time, err error) {
|
||||
timestamp, err := strconv.ParseInt(ds, 10, 64)
|
||||
func Parse(rawInput string) (t time.Time, err error) {
|
||||
timestamp, err := strconv.ParseInt(rawInput, 10, 64)
|
||||
if err == nil {
|
||||
return time.Unix(timestamp, 0), nil
|
||||
}
|
||||
|
||||
ds = replaceNonEnglishWords(ds)
|
||||
d := strings.TrimSpace(ds)
|
||||
if d == "" {
|
||||
return t, errors.New("date parser: empty value")
|
||||
processedInput := invalidLocalizedDateReplacer.Replace(rawInput)
|
||||
processedInput = invalidTimezoneReplacer.Replace(processedInput)
|
||||
processedInput = strings.TrimSpace(processedInput)
|
||||
if processedInput == "" {
|
||||
return t, errors.New(`date parser: empty value`)
|
||||
}
|
||||
|
||||
for _, layout := range dateFormats {
|
||||
switch layout {
|
||||
case time.RFC822, time.RFC850, time.RFC1123:
|
||||
if t, err = parseLocalTimeDates(layout, d); err == nil {
|
||||
if t, err = parseLocalTimeDates(layout, processedInput); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if t, err = time.Parse(layout, d); err == nil {
|
||||
if t, err = time.Parse(layout, processedInput); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lastSpace := strings.LastIndex(ds, " ")
|
||||
if lastSpace > 0 {
|
||||
return Parse(ds[0:lastSpace])
|
||||
}
|
||||
|
||||
err = fmt.Errorf(`date parser: failed to parse date "%s"`, ds)
|
||||
err = fmt.Errorf(`date parser: failed to parse date "%s"`, rawInput)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -242,38 +336,9 @@ func parseLocalTimeDates(layout, ds string) (t time.Time, err error) {
|
||||
loc := time.UTC
|
||||
|
||||
// Workaround for dates that don't use GMT.
|
||||
if strings.HasSuffix(ds, "PST") {
|
||||
if strings.HasSuffix(ds, "PST") || strings.HasSuffix(ds, "PDT") {
|
||||
loc, _ = time.LoadLocation("America/Los_Angeles")
|
||||
}
|
||||
|
||||
return time.ParseInLocation(layout, ds, loc)
|
||||
}
|
||||
|
||||
// Replace German and French dates to English.
|
||||
func replaceNonEnglishWords(ds string) string {
|
||||
r := strings.NewReplacer(
|
||||
"Mo,", "Mon,",
|
||||
"Di,", "Tue,",
|
||||
"Mi,", "Wed,",
|
||||
"Do,", "Thu,",
|
||||
"Fr,", "Fri,",
|
||||
"Sa,", "Sat,",
|
||||
"So,", "Sun,",
|
||||
"Mär ", "Mar ",
|
||||
"Mai ", "May ",
|
||||
"Okt ", "Oct ",
|
||||
"Dez ", "Dec ",
|
||||
"lun,", "Mon,",
|
||||
"mar,", "Tue,",
|
||||
"mer,", "Wed,",
|
||||
"jeu,", "Thu,",
|
||||
"ven,", "Fri,",
|
||||
"sam,", "Sat,",
|
||||
"dim,", "Sun,",
|
||||
"avr ", "Apr ",
|
||||
"mai ", "May ",
|
||||
"jui ", "Jun ",
|
||||
)
|
||||
|
||||
return r.Replace(ds)
|
||||
}
|
||||
|
||||
@@ -132,11 +132,29 @@ func TestParseWeirdDateFormat(t *testing.T) {
|
||||
"March 30 2020 07:02:38 PM",
|
||||
"Mon, 30 Mar 2020 19:53 +0000",
|
||||
"Mon, 03/30/2020 - 19:19",
|
||||
"2018-12-12T12:12",
|
||||
"2020-11-08T16:20:00-05:00Z",
|
||||
"Nov. 16, 2020, 10:57 a.m.",
|
||||
"Friday 06 November 2020",
|
||||
"Mon, November 16, 2020, 11:12 PM EST",
|
||||
"Lundi, 16. Novembre 2020 - 15:54",
|
||||
"Thu Nov 12 2020 17:00:00 GMT+0000 (Coordinated Universal Time)",
|
||||
"Sat, 11 04 2020 08:51:49 +0100",
|
||||
"Mon, 16th Nov 2020 13:16:28 GMT",
|
||||
"Nov. 2020",
|
||||
"ven., 03 juil. 2020 15:09:58 +0000",
|
||||
"Fri, 26/06/2020",
|
||||
"Thu, 29 Oct 2020 07:36:03 GMT-07:00",
|
||||
"jeu., 02 avril 2020 00:00:00 +0200",
|
||||
"Jan. 4, 2016, 12:37 p.m.",
|
||||
"2018-10-23 04:07:42 +00:00",
|
||||
"5 August, 2019",
|
||||
"mar., 01 déc. 2020 16:11:02 +0000",
|
||||
}
|
||||
|
||||
for _, date := range dates {
|
||||
if _, err := Parse(date); err != nil {
|
||||
t.Fatalf(`Unable to parse date: %q`, date)
|
||||
t.Errorf(`Unable to parse date: %q`, date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-12
@@ -34,16 +34,21 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// CreateFeed fetch, parse and store a new feed.
|
||||
func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool, userAgent, username, password, scraperRules, rewriteRules string) (*model.Feed, error) {
|
||||
func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool, userAgent, username, password, scraperRules, rewriteRules, blocklistRules, keeplistRules string, fetchViaProxy bool) (*model.Feed, error) {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Handler:CreateFeed] feedUrl=%s", url))
|
||||
|
||||
if !h.store.CategoryExists(userID, categoryID) {
|
||||
return nil, errors.NewLocalizedError(errCategoryNotFound)
|
||||
}
|
||||
|
||||
request := client.New(url)
|
||||
request := client.NewClientWithConfig(url, config.Opts)
|
||||
request.WithCredentials(username, password)
|
||||
request.WithUserAgent(userAgent)
|
||||
|
||||
if fetchViaProxy {
|
||||
request.WithProxy()
|
||||
}
|
||||
|
||||
response, requestErr := browser.Exec(request)
|
||||
if requestErr != nil {
|
||||
return nil, requestErr
|
||||
@@ -53,14 +58,14 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool,
|
||||
return nil, errors.NewLocalizedError(errDuplicate, response.EffectiveURL)
|
||||
}
|
||||
|
||||
subscription, parseErr := parser.ParseFeed(response.BodyAsString())
|
||||
subscription, parseErr := parser.ParseFeed(response.EffectiveURL, response.BodyAsString())
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
|
||||
subscription.UserID = userID
|
||||
subscription.WithCategoryID(categoryID)
|
||||
subscription.WithBrowsingParameters(crawler, userAgent, username, password, scraperRules, rewriteRules)
|
||||
subscription.WithBrowsingParameters(crawler, userAgent, username, password, scraperRules, rewriteRules, blocklistRules, keeplistRules, fetchViaProxy)
|
||||
subscription.WithClientResponse(response)
|
||||
subscription.CheckedNow()
|
||||
|
||||
@@ -72,11 +77,11 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool,
|
||||
|
||||
logger.Debug("[Handler:CreateFeed] Feed saved with ID: %d", subscription.ID)
|
||||
|
||||
checkFeedIcon(h.store, subscription.ID, subscription.SiteURL)
|
||||
checkFeedIcon(h.store, subscription.ID, subscription.SiteURL, fetchViaProxy)
|
||||
return subscription, nil
|
||||
}
|
||||
|
||||
// RefreshFeed fetch and update a feed if necessary.
|
||||
// RefreshFeed refreshes a feed.
|
||||
func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Handler:RefreshFeed] feedID=%d", feedID))
|
||||
userLanguage := h.store.UserLanguage(userID)
|
||||
@@ -103,7 +108,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
originalFeed.CheckedNow()
|
||||
originalFeed.ScheduleNextCheck(weeklyEntryCount)
|
||||
|
||||
request := client.New(originalFeed.FeedURL)
|
||||
request := client.NewClientWithConfig(originalFeed.FeedURL, config.Opts)
|
||||
request.WithCredentials(originalFeed.Username, originalFeed.Password)
|
||||
request.WithUserAgent(originalFeed.UserAgent)
|
||||
|
||||
@@ -111,6 +116,10 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
request.WithCacheHeaders(originalFeed.EtagHeader, originalFeed.LastModifiedHeader)
|
||||
}
|
||||
|
||||
if originalFeed.FetchViaProxy {
|
||||
request.WithProxy()
|
||||
}
|
||||
|
||||
response, requestErr := browser.Exec(request)
|
||||
if requestErr != nil {
|
||||
originalFeed.WithError(requestErr.Localize(printer))
|
||||
@@ -118,10 +127,17 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
return requestErr
|
||||
}
|
||||
|
||||
if h.store.AnotherFeedURLExists(userID, originalFeed.ID, response.EffectiveURL) {
|
||||
storeErr := errors.NewLocalizedError(errDuplicate, response.EffectiveURL)
|
||||
originalFeed.WithError(storeErr.Error())
|
||||
h.store.UpdateFeedError(originalFeed)
|
||||
return storeErr
|
||||
}
|
||||
|
||||
if originalFeed.IgnoreHTTPCache || response.IsModified(originalFeed.EtagHeader, originalFeed.LastModifiedHeader) {
|
||||
logger.Debug("[Handler:RefreshFeed] Feed #%d has been modified", feedID)
|
||||
|
||||
updatedFeed, parseErr := parser.ParseFeed(response.BodyAsString())
|
||||
updatedFeed, parseErr := parser.ParseFeed(response.EffectiveURL, response.BodyAsString())
|
||||
if parseErr != nil {
|
||||
originalFeed.WithError(parseErr.Localize(printer))
|
||||
h.store.UpdateFeedError(originalFeed)
|
||||
@@ -132,7 +148,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
processor.ProcessFeedEntries(h.store, originalFeed)
|
||||
|
||||
// We don't update existing entries when the crawler is enabled (we crawl only inexisting entries).
|
||||
if storeErr := h.store.UpdateEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, !originalFeed.Crawler); storeErr != nil {
|
||||
if storeErr := h.store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, !originalFeed.Crawler); storeErr != nil {
|
||||
originalFeed.WithError(storeErr.Error())
|
||||
h.store.UpdateFeedError(originalFeed)
|
||||
return storeErr
|
||||
@@ -141,7 +157,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
// We update caching headers only if the feed has been modified,
|
||||
// because some websites don't return the same headers when replying with a 304.
|
||||
originalFeed.WithClientResponse(response)
|
||||
checkFeedIcon(h.store, originalFeed.ID, originalFeed.SiteURL)
|
||||
checkFeedIcon(h.store, originalFeed.ID, originalFeed.SiteURL, originalFeed.FetchViaProxy)
|
||||
} else {
|
||||
logger.Debug("[Handler:RefreshFeed] Feed #%d not modified", feedID)
|
||||
}
|
||||
@@ -162,9 +178,9 @@ func NewFeedHandler(store *storage.Storage) *Handler {
|
||||
return &Handler{store}
|
||||
}
|
||||
|
||||
func checkFeedIcon(store *storage.Storage, feedID int64, websiteURL string) {
|
||||
func checkFeedIcon(store *storage.Storage, feedID int64, websiteURL string, fetchViaProxy bool) {
|
||||
if !store.HasIcon(feedID) {
|
||||
icon, err := icon.FindIcon(websiteURL)
|
||||
icon, err := icon.FindIcon(websiteURL, fetchViaProxy)
|
||||
if err != nil {
|
||||
logger.Debug("CheckFeedIcon: %v (feedID=%d websiteURL=%s)", err, feedID, websiteURL)
|
||||
} else if icon == nil {
|
||||
|
||||
+13
-6
@@ -11,6 +11,7 @@ import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/crypto"
|
||||
"miniflux.app/http/client"
|
||||
"miniflux.app/logger"
|
||||
@@ -21,9 +22,12 @@ import (
|
||||
)
|
||||
|
||||
// FindIcon try to find the website's icon.
|
||||
func FindIcon(websiteURL string) (*model.Icon, error) {
|
||||
func FindIcon(websiteURL string, fetchViaProxy bool) (*model.Icon, error) {
|
||||
rootURL := url.RootURL(websiteURL)
|
||||
clt := client.New(rootURL)
|
||||
clt := client.NewClientWithConfig(rootURL, config.Opts)
|
||||
if fetchViaProxy {
|
||||
clt.WithProxy()
|
||||
}
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to download website index page: %v", err)
|
||||
@@ -43,7 +47,7 @@ func FindIcon(websiteURL string) (*model.Icon, error) {
|
||||
}
|
||||
|
||||
logger.Debug("[FindIcon] Fetching icon => %s", iconURL)
|
||||
icon, err := downloadIcon(iconURL)
|
||||
icon, err := downloadIcon(iconURL, fetchViaProxy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -68,7 +72,7 @@ func parseDocument(websiteURL string, data io.Reader) (string, error) {
|
||||
for _, query := range queries {
|
||||
doc.Find(query).Each(func(i int, s *goquery.Selection) {
|
||||
if href, exists := s.Attr("href"); exists {
|
||||
iconURL = href
|
||||
iconURL = strings.TrimSpace(href)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -86,8 +90,11 @@ func parseDocument(websiteURL string, data io.Reader) (string, error) {
|
||||
return iconURL, nil
|
||||
}
|
||||
|
||||
func downloadIcon(iconURL string) (*model.Icon, error) {
|
||||
clt := client.New(iconURL)
|
||||
func downloadIcon(iconURL string, fetchViaProxy bool) (*model.Icon, error) {
|
||||
clt := client.NewClientWithConfig(iconURL, config.Opts)
|
||||
if fetchViaProxy {
|
||||
clt.WithProxy()
|
||||
}
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to download iconURL: %v", err)
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
package icon // import "miniflux.app/reader/icon"
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseImageDataURL(t *testing.T) {
|
||||
iconURL := "data:image/webp;base64,UklGRhQJAABXRUJQVlA4TAcJAAAvv8AvEIU1atuOza3OCSaanSeobUa17T61bdu2bVtRbdvtDmrb7gSTdibJXOG81/d9z/vsX3utCLi1bbuJ3hKeVEymRRuaSnCVSBWIBmwP410h0IHJXDyfZCfRNhklFS/sufGPbPHPjT0vVJRkhE1BwxFZ5EhDQVjkrEjIJokVOVHMhAuyyoUpUUCbDbLLhjbRFkO+kWG+GRLT0+YTWeaTNjEdW2SaLTEtU2SbOTGVnAuyzY0nYgobZJwtMZkxD2ScB2NiEg2yTkOQcULWOZFRIvOU1Mg8FS/IPC8ckHkOXJF5riRknoT/pb1t6iwPetFIH3jNY660i/khw/3dq4W09ZbNIbN1TjOeFD2iB2T1KmIM0x0yuhOxbod81vueWK0GQDa3IuZ1kM2bifkdZPM94s4CuRxN3GUhl2KvC7kUez3I5TjiLge5/Ji4s0AuBxPzO8jmbsS8GrLZ4G9itVoM8nkssW6CjLb3BDFGaoCcdnU/KXxMb8hrnZ18Ttr82UHqILvtrO50j/vOaDKpyY/ecKWNdYJst1MP/7fxHwtYyprWtrGNrG0pfcyqDjI7r22d6V4faCJttfjOa4Y6155WMwuUpsEw5spQjW62d7tvif+H4YapCAkFYkaofB1DNJEaIqFAzAgVdrCTkaS2SCgQM0Jla/uQ1BoJBWJGqKTBTaT2SCgQM0IFfXxMEkBCgZgR/I2MJSkgoUDMCPaWmkkSSCgQM4K7pmaSBhIKxIxgLqCRJIKEAjEjePWGk1SQUCBmBO8kksgoj0BCgZgRrDn8Q+zfDXKkzaxt0gb2coX3SMVNnnG85XSAlAIxI1hXEneEzbWH6fsYpJX4zV52mlXVQ2qBmBGcWY0jXquTdYC21/En8YY7z7q6QoqBmBGc44jXag8o7Ot3Yp0DiQZiRnDeI97FYGyglTj/mgvSDMSMYCxGvG91BWcQsa6BNAMxIxgHEe9gsBbVSpwxekCSgZgRjCHEGqcBvBeJtRckGYgZwfiGWA+CeSixnoAkAzEjFDcQ73AwBxCrST2kGIgZobgP8VYDs4MWYi0LKQZiRihej3izgvsZsfaEFAMxIxRvR6yJ2oP7IrFOhxQDMSMU70+sRrAfIdYNkGIgZoTi/Yn1I9gDiTUQUgzEjFC8P7F+BHsgsQZCioGYEYp3IlYj2A8TayCkGIgZoXgT4nUE91ViXQ0pBmJGKF6GePOC+w2xTocUAzEjFPcm3sZgdtNKrH0gxUDMCMZvxDoXzDWJtxqkGIgZwXicWO+CeT6xWvWCFAMxIxgnEm9xsNr5mlifQJKBmBGMJYl3K1hbEO8aSDIQM4JR52tiTbQMGPU+It56kGQgZgTndOJ9JEDxecT7XntIMhAzgjO7ZuI9rwGK9tJKvLMhzUDMCNZNxHxXP2izi0u0Em+cWSHNQMwI1hyaiDneXVbTHqad0zF+IO4FkGggZgTveOKP9qLbXOo813vYl8T/XW9INBAzgtfBf0ntdoBUAzEjmPP5m9TqVkg2EDOCu6ZmUps3dYFkAzEj2NtoIbV4z4yQbiBmBH9jY0j1R5gJEg7EjFBBHx+Taj+kAVIOxIxQSReXGU+q2ewYdZB0IGaEyhZzj4mkam/oD4kHYkaosI8PSJW+tb06SD0QM0JFnZyjhVRnuJ3UQ/qBmBEqWcQIUpU/3GAVKEUgZoQKttNEKh/nZWdaVXsoSSBmBP8kraToAdd51Pt+MoZM86v3PetOZ9hBfx2hRIGYEewzSeFZ6mBqnZ4mBShlIGYE9xBSeAOUPRAzgtlfCyn6UTcoeyBmBPNZUngalD4QM4LXjxRvDKUPxIzgnUCKl4XSB2JG8J4kxftB6QMxI3jfkeIfzQ9lD8SM4I0hxm/2UQ/lDsSM4I0i1p/usLul9IDyBmJG8D4jfpPvfekDwxS95RlPutMljrGlxdRD2oGYEbyHSU1a/Ncl1tcR0g3EjODtT2r2l1stC6kGYkbwehhDavi69SHNQMwI5mmkpk+YF1IMxIxgdvIBqWmj7SDBQMwIbl+NpLZnQHqBmBHsdTST2l4GyQViRvDXMprU9hhILRAzQgWLGkZqOsFqkFggZoRKOtrPd6SWX+oMaQViRqhgUcd7QTOp6dGQViBmBLeXw71Pav6LLpBUIGYEb1aXaSIp7AlJBWJGcDo50RiSxtOQVCBmBKOv90gqE/SClAIxIxRvbSxJZyNIqZ35mF2hcC8TSUJnQwm30krMH93jOJtYTX/zaXNhS5m0lq0c7GxDfWoi8R+B8vXRRKx/3GpVdVBBd1sYrImY70PpOhhJrEHmgIpncivxfofSHUCcJttBVU4g1hgoW72fiNFkFajSY8RC2XYkzh5QrRWJhbI9SIxXoGp1GokxHkpWbxwxNoPqDSPGL1CyZYgxXheo3hvEeBdKthMxPoYqfkaMB6BkJxHjVaheMIEYZ0HJziXGO1C9vYizBZTscmKM1R6q1cnnxJioN5TsLOKsCdW6ljhvQtmOIc7jUKVTiXUElG0HYu0O1ejhJmI1mxHKNoBYzTaFiuvs4mfi3Qql6+RfYk10tk5QUXube4OY4y0I5XuUmF/bUxdwO1jRxb4n9uVQwn2J/ZdbbWNWKGpnXhs42SMaSQXfC1DCHhpJJT97we0uca5jHeJYk45znmsN9JJP/UsqnGAtKOWFJJ2ToZwz+J2kcqs6KOkuJJGB2kNZ69xFkrhaeyhvF2+S2v/jICh1T6+TWn9qAJS8m8dITce7WAOUvs6xWkjtnrEYZGFpw0mNXrMB5KKdPXxNqj/OIMtDTjra0eukqhM9azcBsrOg03xMqvSLIXYzM2RqAfu600cmkIr+9oKL7GQRyFyDFe3hDHd4xcd+NZ601ehbIzzuNqfbyxrmhKx219Ns5jN5bj1N6g6pkZB5EldknisHZJ4DL8g8L9TIPBXPyDwlGSdknRMZQYOs0xCTKEjIOImCmMwKGWdDTCHnimxzJSemMkO2WRDTskWm2RHT0eUTWeaTLjE9Q/6QYX4YEm3RYYvssqVDFDDjgqxyYU4UM2JDQjZJbBgRFgVLzsgiZ5YUhE1GSc0Le+48kC0e3NnzQk1JRrQNAA=="
|
||||
@@ -49,3 +52,18 @@ func TestParseInvalidImageDataURL(t *testing.T) {
|
||||
t.Fatal(`We should detect malformed image data URL`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDocumentWithWhitespaceIconURL(t *testing.T) {
|
||||
html := `<link rel="shortcut icon" href="
|
||||
/static/img/favicon.ico
|
||||
">`
|
||||
|
||||
iconURL, err := parseDocument("http://www.example.org/", strings.NewReader(html))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if iconURL != "http://www.example.org/static/img/favicon.ico" {
|
||||
t.Errorf(`Invalid icon URL, got %q`, iconURL)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-5
@@ -55,12 +55,22 @@ func (j *jsonFeed) GetAuthor() string {
|
||||
return getAuthor(j.Author)
|
||||
}
|
||||
|
||||
func (j *jsonFeed) Transform() *model.Feed {
|
||||
feed := new(model.Feed)
|
||||
feed.FeedURL = j.FeedURL
|
||||
feed.SiteURL = j.SiteURL
|
||||
feed.Title = strings.TrimSpace(j.Title)
|
||||
func (j *jsonFeed) Transform(baseURL string) *model.Feed {
|
||||
var err error
|
||||
|
||||
feed := new(model.Feed)
|
||||
|
||||
feed.FeedURL, err = url.AbsoluteURL(baseURL, j.FeedURL)
|
||||
if err != nil {
|
||||
feed.FeedURL = j.FeedURL
|
||||
}
|
||||
|
||||
feed.SiteURL, err = url.AbsoluteURL(baseURL, j.SiteURL)
|
||||
if err != nil {
|
||||
feed.SiteURL = j.SiteURL
|
||||
}
|
||||
|
||||
feed.Title = strings.TrimSpace(j.Title)
|
||||
if feed.Title == "" {
|
||||
feed.Title = feed.SiteURL
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
)
|
||||
|
||||
// Parse returns a normalized feed struct from a JON feed.
|
||||
func Parse(data io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
func Parse(baseURL string, data io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
feed := new(jsonFeed)
|
||||
decoder := json.NewDecoder(data)
|
||||
if err := decoder.Decode(&feed); err != nil {
|
||||
return nil, errors.NewLocalizedError("Unable to parse JSON Feed: %q", err)
|
||||
}
|
||||
|
||||
return feed.Transform(), nil
|
||||
return feed.Transform(baseURL), nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user