Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22dbc32f23 | ||
|
|
9043cbcefe | ||
|
|
e969f04084 | ||
|
|
f490f36e56 | ||
|
|
d40eb65086 | ||
|
|
7d113e026e | ||
|
|
38892d8a4a | ||
|
|
77d87aeecd | ||
|
|
22052bd38f | ||
|
|
2fd053a077 | ||
|
|
97636b23f5 | ||
|
|
f705e879a1 | ||
|
|
6836dedda4 | ||
|
|
88f0f6e4c0 | ||
|
|
9b0c06d98b | ||
|
|
0757c4e7fb | ||
|
|
1ec77590e9 | ||
|
|
e439d96e43 | ||
|
|
1aa94292db | ||
|
|
3551fd745a | ||
|
|
785d89d6ab | ||
|
|
6a8759e21e | ||
|
|
a3a3d367ea | ||
|
|
e4bbd756f0 | ||
|
|
2782ced03d | ||
|
|
45a84df5b4 | ||
|
|
57273946c3 | ||
|
|
572e834999 | ||
|
|
f1541368e5 | ||
|
|
9936c6bf1e | ||
|
|
a97d9cdbf4 | ||
|
|
ef004675a5 | ||
|
|
3f5f4e01f1 | ||
|
|
c185cb8933 | ||
|
|
1c1e3a8919 | ||
|
|
fc9a75ce8f | ||
|
|
2bf0e9b7ce | ||
|
|
e1303c95d0 | ||
|
|
915287dc10 | ||
|
|
86590d2990 | ||
|
|
3179762fd3 | ||
|
|
86ad88fb09 | ||
|
|
db6d6a5eba | ||
|
|
7a75c5ecfa | ||
|
|
dfe70dc4cf | ||
|
|
afbecd9a1e | ||
|
|
7f4be43bb4 | ||
|
|
05eccfa8e7 | ||
|
|
a6c4183aac | ||
|
|
7fc8577ce0 | ||
|
|
a1524af7a9 | ||
|
|
10cf7e6e25 | ||
|
|
7ce94eea59 | ||
|
|
8eb8f4e231 | ||
|
|
af19268d6f | ||
|
|
4215d7e43b | ||
|
|
d85917bfb2 | ||
|
|
7c1d6e8bfd | ||
|
|
3a2fd2374a | ||
|
|
87a6113cc7 | ||
|
|
4cb7dc2d30 | ||
|
|
f52310a841 | ||
|
|
97707f7cca | ||
|
|
5058ca8f27 | ||
|
|
b3a856a05e | ||
|
|
f557bd45df | ||
|
|
2f8435892e | ||
|
|
4bba26daf6 | ||
|
|
3c97901995 | ||
|
|
3828022a1c | ||
|
|
85e2da054b | ||
|
|
0753c5cb54 | ||
|
|
845e28dd59 | ||
|
|
eff88711ea | ||
|
|
8466e50cc3 | ||
|
|
c9fd58c904 | ||
|
|
47869a77c9 | ||
|
|
246f12c8cb | ||
|
|
943212e492 | ||
|
|
7a6fb98223 | ||
|
|
3444fb9b75 | ||
|
|
be46304e23 | ||
|
|
5fa55dfbba | ||
|
|
09fb647f03 | ||
|
|
d518a44d32 | ||
|
|
dcacde0959 | ||
|
|
064d4d53f6 | ||
|
|
aec7ee2708 | ||
|
|
10fd170833 | ||
|
|
ba03b8df38 | ||
|
|
ef45f3d0ca | ||
|
|
b1acea9f1c | ||
|
|
7059d5c834 | ||
|
|
1539499294 | ||
|
|
6f587f4757 | ||
|
|
632206e172 | ||
|
|
2eeb9283f0 | ||
|
|
d137820037 | ||
|
|
4eedfdcf6f | ||
|
|
bae18ecb0a | ||
|
|
2b9eb8e127 | ||
|
|
05523f6bb1 | ||
|
|
30ca090e74 | ||
|
|
fa8e068e24 | ||
|
|
72275e7573 | ||
|
|
5b278e2e86 | ||
|
|
6c450c6f3b | ||
|
|
dd050bb6a0 | ||
|
|
c7a6b05c6d | ||
|
|
35297a287a | ||
|
|
85b51bf3c9 | ||
|
|
9dff67f003 | ||
|
|
a5ea522e45 | ||
|
|
61e274e56d | ||
|
|
c20ed60913 | ||
|
|
b31d95c7f6 | ||
|
|
689fd21afa | ||
|
|
be3580f7b1 | ||
|
|
6085471b81 | ||
|
|
3943b53163 | ||
|
|
fe674ac88b | ||
|
|
9c29e86222 | ||
|
|
933ba2da0d | ||
|
|
4d0b75ed0e | ||
|
|
1dcb900cf3 | ||
|
|
46dea89b41 | ||
|
|
977fc9db28 | ||
|
|
3e83700fc2 | ||
|
|
0d7305d9ba | ||
|
|
d4eed91130 | ||
|
|
ffafde2b3e | ||
|
|
a7b346d8df | ||
|
|
25316d7bf2 | ||
|
|
4f623881ac | ||
|
|
b5cd49db4c | ||
|
|
89685bfac6 | ||
|
|
319a89387a | ||
|
|
af3aab21e3 | ||
|
|
fb407bcbce |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +1,4 @@
|
||||
templates/**/* linguist-vendored
|
||||
public/**/*.css linguist-vendored
|
||||
public/**/*.scss linguist-vendored
|
||||
*.config.js linguist-vendored
|
||||
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: thomiceli
|
||||
77
.github/workflows/go.yml
vendored
77
.github/workflows/go.yml
vendored
@@ -1,49 +1,68 @@
|
||||
name: "Go"
|
||||
name: "Go CI"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 'dev-*'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.yml'
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.22"
|
||||
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.54
|
||||
skip-pkg-cache: true
|
||||
args: --out-format=colored-line-number --timeout=20m
|
||||
|
||||
- name: Format
|
||||
run: make fmt check_changes
|
||||
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.22"
|
||||
|
||||
- name: Check Go modules
|
||||
run: make go_mod check_changes
|
||||
|
||||
- name: Check translations
|
||||
run: make check-tr
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macOS-latest"]
|
||||
go: ["1.19", "1.20"]
|
||||
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||
go: ["1.22"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: WillAbides/setup-go-faster@v1.8.0
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Run tests
|
||||
run: make test
|
||||
|
||||
- name: Cache Go build cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-build-${{ matrix.go }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-build-
|
||||
|
||||
- name: Run go vet
|
||||
run: "go vet ./..."
|
||||
|
||||
- name: Run Staticcheck
|
||||
uses: dominikh/staticcheck-action@v1.3.0
|
||||
with:
|
||||
version: "2023.1.1"
|
||||
install-go: false
|
||||
cache-key: ${{ matrix.go }}
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
name: Docker
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
binaries-build-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.22"
|
||||
|
||||
- name: Cross compile build
|
||||
run: make all_crosscompile
|
||||
|
||||
- name: Upload Release Assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
build/*.tar.gz
|
||||
build/*.zip
|
||||
build/checksums.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker-build-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ gist.db
|
||||
public/assets/*
|
||||
public/manifest.json
|
||||
opengist
|
||||
build/
|
||||
|
||||
199
CHANGELOG.md
199
CHANGELOG.md
@@ -1,5 +1,204 @@
|
||||
# Changelog
|
||||
|
||||
## [1.7.3](https://github.com/thomiceli/opengist/compare/v1.7.2...v1.7.3) - 2024-06-03
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Setting to allow anonymous access to individual gists while still RequireLogin everywhere else (#229)
|
||||
- Make edit visibility a toggle (#277)
|
||||
- More translation strings (#274) (#281)
|
||||
- String method to visibility (#276)
|
||||
|
||||
### Fixed
|
||||
- Perms for http/ssh clone (#288)
|
||||
- Fix translation string (#293)
|
||||
|
||||
### Other
|
||||
- Update deps Golang & JS deps
|
||||
- Check translations keys in CI (#279)
|
||||
- Fix CI check for additional translations only (#289)
|
||||
|
||||
## [1.7.2](https://github.com/thomiceli/opengist/compare/v1.7.1...v1.7.2) - 2024-05-05
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Docs:
|
||||
- Run with systemd as a normal user (#254)
|
||||
- Kubernetes deployment (#258)
|
||||
- More translation strings (#269) (#271)
|
||||
|
||||
### Changed
|
||||
- Rework git log parsing and truncating (#260)
|
||||
- Set Opengist version from git tags (#261)
|
||||
|
||||
### Fixed
|
||||
- Missing preview button when editing .md gist (#259)
|
||||
- Frontend (#267)
|
||||
- Fix mermaid display
|
||||
- Move Login/Register buttons on mobile
|
||||
- Set minimum width on avatar
|
||||
|
||||
### Other
|
||||
- Use go 1.22 and update deps (#244)
|
||||
|
||||
## [1.7.1](https://github.com/thomiceli/opengist/compare/v1.7.0...v1.7.1) - 2024-04-05
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Docs: More detailed variant for custom pages (#248)
|
||||
|
||||
### Fixed
|
||||
- Auth page GitlabName Error (#242)
|
||||
- Empty invitation on user creation (#247)
|
||||
|
||||
## [1.7.0](https://github.com/thomiceli/opengist/compare/v1.6.1...v1.7.0) - 2024-04-03
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
Note: all sessions will be invalidated after this update.
|
||||
|
||||
### Added
|
||||
- Custom logo configuration (#209)
|
||||
- Custom static links (#234)
|
||||
- Invitations for closed registrations (#233)
|
||||
- Set gist visibility via Git push options (#215)
|
||||
- Set gist URL and title via push options (#216)
|
||||
- Specify custom names in the OAuth login buttons (#214)
|
||||
- Markdown preview (#224)
|
||||
- Reset a user password using CLI (#226)
|
||||
- Translations (#207, #210)
|
||||
|
||||
### Changed
|
||||
- Use filesystem session store (#240)
|
||||
- Move Git hook logic to Opengist (#213)
|
||||
- Increase login for 1 year (#222)
|
||||
|
||||
### Fixed
|
||||
- Show theme change button on responsive devices (#225)
|
||||
- New line literal in embed gists (#237)
|
||||
|
||||
### Other
|
||||
- GitHub security updates
|
||||
- New docker dev env (#220)
|
||||
|
||||
## [1.6.1](https://github.com/thomiceli/opengist/compare/v1.6.0...v1.6.1) - 2024-01-06
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Healthcheck on Docker container (#204)
|
||||
- Translations:
|
||||
- fr-FR (#201)
|
||||
|
||||
### Fixed
|
||||
- Directory renaming on username change (#205)
|
||||
|
||||
## [1.6.0](https://github.com/thomiceli/opengist/compare/v1.5.3...v1.6.0) - 2024-01-04
|
||||
See here how to [update](/docs/update.md) Opengist.
|
||||
|
||||
### Added
|
||||
- Embedded gists (#179)
|
||||
- Gist code search (#194)
|
||||
- Custom URLS for gists (#183)
|
||||
- Gist JSON data/metadata (#179)
|
||||
- Keep default visibility when creating a gist on the UI (#155)
|
||||
- Health check endpoint (#170)
|
||||
- GitLab OAuth2 login (#174)
|
||||
- Syntax highlighting for more file types (#176)
|
||||
- Checkable Markdown checkboxes (#182)
|
||||
- Config:
|
||||
- Log output (#172)
|
||||
- Default git branch name (#171)
|
||||
- Change username setting (#190)
|
||||
- Admin actions:
|
||||
- Synchronize all gists previews (#191)
|
||||
- Reset Git server hooks for all repositories (#191)
|
||||
- Index all gists (#194)
|
||||
- Translations:
|
||||
- cs-CZ (#164)
|
||||
- zh-TW (#166, #195)
|
||||
- hu-HU (#185)
|
||||
- pt-BR (#193)
|
||||
- Docs (#198)
|
||||
|
||||
### Changed
|
||||
- Updated dependencies (#197):
|
||||
- Go `1.20` -> `1.21`
|
||||
- JavaScript packages
|
||||
- NodeJS Docker image `18` -> `20`
|
||||
- Alpine Docker image `3.17` -> `3.19`
|
||||
|
||||
### Fixed
|
||||
- Fix reverse proxy subpath support (#192)
|
||||
- Fix undecoded gist content when going back to editing in the UI (#184)
|
||||
- Fix outputting non-truncated large files for editon/zip download (#184)
|
||||
- Allow dashes in usernames (#184)
|
||||
- Delete SSH keys associated to deleted user (#184)
|
||||
- Better error message when there is no files in gist (#184)
|
||||
- Show if there is no files in gist preview (#184)
|
||||
- Log parsing for the 11th empty commit (#184)
|
||||
- Optimize reading gist files content (#186)
|
||||
|
||||
## [1.5.3](https://github.com/thomiceli/opengist/compare/v1.5.2...v1.5.3) - 2023-11-20
|
||||
### Added
|
||||
- es-ES translation (#139)
|
||||
- Create/change account password (#156)
|
||||
- Display OAuth error messages when HTTP 400 (#159)
|
||||
|
||||
### Fixed
|
||||
- Git bare repository branch name creation (#157)
|
||||
- Git file truncated output hanging (#157)
|
||||
- Home user directory detection handling (#145)
|
||||
- UI changes (#158)
|
||||
|
||||
## [1.5.2](https://github.com/thomiceli/opengist/compare/v1.5.1...v1.5.2) - 2023-10-16
|
||||
### Added
|
||||
- zh-CN translation (#130)
|
||||
- ru-RU translation (#135)
|
||||
- config.yml usage in the Docker container (#131)
|
||||
- Longer title and description (#129)
|
||||
|
||||
### Fixed
|
||||
- Private gist visibility (#128)
|
||||
- Dark background color in Markdown rendering (#137)
|
||||
- Error handling for password hashes (#132)
|
||||
|
||||
## [1.5.1](https://github.com/thomiceli/opengist/compare/v1.5.0...v1.5.1) - 2023-09-29
|
||||
### Added
|
||||
- Hungarian translations (#123)
|
||||
|
||||
### Fixed
|
||||
- .c and .h syntax highlighting (#119)
|
||||
- Login page disabled depending on locale (#120)
|
||||
- Syntax error on templates when calling locale function (#122)
|
||||
|
||||
## [1.5.0](https://github.com/thomiceli/opengist/compare/v1.4.2...v1.5.0) - 2023-09-26
|
||||
### Added
|
||||
- Private Gist visibility (#87)
|
||||
- Create gists from a special Git HTTP server remote URL (#95)
|
||||
- OIDC provider integration (#98)
|
||||
- Translation system (#104)
|
||||
- Run `git gc` on all repositories as admin (#90)
|
||||
- Unit and integration tests (#97)
|
||||
- Documentation (#110, #111)
|
||||
- New logo (#103)
|
||||
|
||||
### Changed
|
||||
- Use Non-CGO SQLite instead of CGO SQLite (#100)
|
||||
- Various UI changes (#84, #93)
|
||||
- Improved CI/CD pipeline (#99, #113)
|
||||
- Improved git http semantics and repo obfuscation (#94)
|
||||
- Updated Go deps (#102)
|
||||
|
||||
### Fixed
|
||||
- Find command for Windows users (#89)
|
||||
- Retain visibility when editing a gist (#83)
|
||||
- Typo on admin index page (#85)
|
||||
- ViteJS dev server (#91)
|
||||
- Bugs (#105)
|
||||
|
||||
### Breaking changes
|
||||
- Removed CONFIG env var
|
||||
- Removed TLS server (#101)
|
||||
|
||||
## [1.4.2](https://github.com/thomiceli/opengist/compare/v1.4.1...v1.4.2) - 2023-07-17
|
||||
### Added
|
||||
- External url to HTML links & redirects (#75)
|
||||
|
||||
43
Dockerfile
43
Dockerfile
@@ -1,16 +1,25 @@
|
||||
FROM alpine:3.17 AS build
|
||||
FROM alpine:3.19 AS base
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
make \
|
||||
gcc \
|
||||
musl-dev \
|
||||
libstdc++
|
||||
apk add --no-cache \
|
||||
make \
|
||||
shadow \
|
||||
openssl \
|
||||
openssh \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
gnupg \
|
||||
xz \
|
||||
gcc \
|
||||
musl-dev \
|
||||
libstdc++
|
||||
|
||||
COPY --from=golang:1.19-alpine /usr/local/go/ /usr/local/go/
|
||||
COPY --from=golang:1.22-alpine /usr/local/go/ /usr/local/go/
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
COPY --from=node:18-alpine /usr/local/ /usr/local/
|
||||
COPY --from=node:20-alpine /usr/local/ /usr/local/
|
||||
ENV NODE_PATH="/usr/local/lib/node_modules"
|
||||
ENV PATH="/usr/local/bin:${PATH}"
|
||||
|
||||
@@ -18,10 +27,23 @@ WORKDIR /opengist
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
FROM base AS dev
|
||||
|
||||
EXPOSE 6157 2222 16157
|
||||
VOLUME /opengist
|
||||
|
||||
RUN git config --global --add safe.directory /opengist
|
||||
|
||||
CMD ["make", "watch"]
|
||||
|
||||
|
||||
FROM base AS build
|
||||
|
||||
RUN make
|
||||
|
||||
|
||||
FROM alpine:3.17 as run
|
||||
FROM alpine:3.19 as prod
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
@@ -40,6 +62,8 @@ RUN apk update && \
|
||||
RUN addgroup -S opengist && \
|
||||
adduser -S -G opengist -H -s /bin/ash -g 'Opengist User' opengist
|
||||
|
||||
COPY --from=build --chown=opengist:opengist /opengist/config.yml config.yml
|
||||
|
||||
WORKDIR /app/opengist
|
||||
|
||||
COPY --from=build --chown=opengist:opengist /opengist/opengist .
|
||||
@@ -47,4 +71,5 @@ COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
|
||||
|
||||
EXPOSE 6157 2222
|
||||
VOLUME /opengist
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
|
||||
ENTRYPOINT ["./docker/entrypoint.sh"]
|
||||
|
||||
48
Makefile
48
Makefile
@@ -1,9 +1,13 @@
|
||||
.PHONY: all install build_frontend build_backend build build_docker watch_frontend watch_backend watch clean clean_docker
|
||||
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker build_dev_docker run_dev_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test check-tr
|
||||
|
||||
# Specify the name of your Go binary output
|
||||
BINARY_NAME := opengist
|
||||
GIT_TAG := $(shell git describe --tags)
|
||||
VERSION_PKG := github.com/thomiceli/opengist/internal/config.OpengistVersion
|
||||
|
||||
all: install build
|
||||
all: clean install build
|
||||
|
||||
all_crosscompile: clean install build_frontend build_crosscompile
|
||||
|
||||
install:
|
||||
@echo "Installing NPM dependencies..."
|
||||
@@ -13,34 +17,62 @@ install:
|
||||
|
||||
build_frontend:
|
||||
@echo "Building frontend assets..."
|
||||
npx vite build
|
||||
npx vite -c public/vite.config.js build
|
||||
@EMBED=1 npx postcss 'public/assets/embed-*.css' -c public/postcss.config.js --replace # until we can .nest { @tailwind } in Sass
|
||||
|
||||
build_backend:
|
||||
@echo "Building Opengist binary..."
|
||||
go build -tags fs_embed -o $(BINARY_NAME) .
|
||||
go build -tags fs_embed -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" -o $(BINARY_NAME) .
|
||||
|
||||
build: build_frontend build_backend
|
||||
|
||||
build_crosscompile:
|
||||
@bash ./scripts/build-all.sh
|
||||
|
||||
build_docker:
|
||||
@echo "Building Docker image..."
|
||||
docker build -t $(BINARY_NAME):latest .
|
||||
|
||||
build_dev_docker:
|
||||
@echo "Building Docker image..."
|
||||
docker build -t $(BINARY_NAME)-dev:latest --target dev .
|
||||
|
||||
run_dev_docker:
|
||||
docker run -v .:/opengist -p 6157:6157 -p 16157:16157 -p 2222:2222 -v $(HOME)/.opengist-dev:/root/.opengist --rm $(BINARY_NAME)-dev:latest
|
||||
|
||||
watch_frontend:
|
||||
@echo "Building frontend assets..."
|
||||
npx vite dev --port 16157
|
||||
npx vite -c public/vite.config.js dev --port 16157 --host
|
||||
|
||||
watch_backend:
|
||||
@echo "Building Opengist binary..."
|
||||
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run . --config config.yml'
|
||||
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" . --config config.yml'
|
||||
|
||||
watch:
|
||||
@bash ./watch.sh
|
||||
@sh ./scripts/watch.sh
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up build artifacts..."
|
||||
@rm -f $(BINARY_NAME) public/manifest.json
|
||||
@rm -rf public/assets
|
||||
@rm -rf public/assets build
|
||||
|
||||
clean_docker:
|
||||
@echo "Cleaning up Docker image..."
|
||||
@docker rmi $(BINARY_NAME)
|
||||
|
||||
check_changes:
|
||||
@echo "Checking for changes..."
|
||||
@git --no-pager diff --exit-code || (echo "There are unstaged changes detected." && exit 1)
|
||||
|
||||
go_mod:
|
||||
@go mod download
|
||||
@go mod tidy
|
||||
|
||||
fmt:
|
||||
@go fmt ./...
|
||||
|
||||
test:
|
||||
@go test ./... -p 1
|
||||
|
||||
check-tr:
|
||||
@bash ./scripts/check-translations.sh
|
||||
226
README.md
226
README.md
@@ -1,63 +1,43 @@
|
||||
# Opengist
|
||||
|
||||
<img height="108px" src="https://raw.githubusercontent.com/thomiceli/opengist/a9dd531f676d01b93bb6bd70751a69382ca563b0/public/opengist.svg" alt="Opengist" align="right" />
|
||||
|
||||
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
|
||||
read and/or modified using standard Git commands, or with the web interface.
|
||||
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
|
||||
|
||||
[Documentation](/docs) • [Discord](https://discord.gg/9Pm3X5scZT) • [Demo](https://demo.opengist.io)
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||
[](https://github.com/thomiceli/opengist/actions/workflows/go.yml)
|
||||
[](https://goreportcard.com/report/github.com/thomiceli/opengist)
|
||||
|
||||
A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomice.li).
|
||||
|
||||
* [Features](#features)
|
||||
* [Install](#install)
|
||||
* [With Docker](#with-docker)
|
||||
* [From source](#from-source)
|
||||
* [Configuration](#configuration)
|
||||
* [Via YAML file](#configuration-via-yaml-file)
|
||||
* [Via Environment Variables](#configuration-via-environment-variables)
|
||||
* [Administration](#administration)
|
||||
* [Use Nginx as a reverse proxy](#use-nginx-as-a-reverse-proxy)
|
||||
* [Use Fail2ban](#use-fail2ban)
|
||||
* [Configure OAuth](#configure-oauth)
|
||||
* [License](#license)
|
||||
|
||||
## Features
|
||||
|
||||
* Create public or unlisted snippets
|
||||
* Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||
* Revisions history
|
||||
* Create public, unlisted or private snippets
|
||||
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||
* Syntax highlighting ; markdown & CSV support
|
||||
* Search code in snippets ; browse users snippets, likes and forks
|
||||
* Embed snippets in other websites
|
||||
* Revisions history
|
||||
* Like / Fork snippets
|
||||
* Search for snippets ; browse users snippets, likes and forks
|
||||
* Editor with indentation mode & size ; drag and drop files
|
||||
* Download raw files or as a ZIP archive
|
||||
* OAuth2 login with GitHub and Gitea
|
||||
* Avatars via Gravatar or OAuth2 providers
|
||||
* Light/Dark mode
|
||||
* Responsive UI
|
||||
* Enable or disable signups
|
||||
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
|
||||
* Restrict or unrestrict snippets visibility to anonymous users
|
||||
* Admin panel : delete users/gists; clean database/filesystem by syncing gists
|
||||
* SQLite database
|
||||
* Logging
|
||||
* Docker support
|
||||
* [More...](/docs/index.md#features)
|
||||
|
||||
#### Todo
|
||||
|
||||
- [ ] Translation
|
||||
- [ ] Code/text search
|
||||
- [ ] Embed snippets
|
||||
- [ ] Tests
|
||||
- [ ] Filesystem/Redis support for user sessions
|
||||
- [ ] Have a cool logo
|
||||
|
||||
## Install
|
||||
## Quick start
|
||||
|
||||
### With Docker
|
||||
|
||||
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/thomiceli/opengist:1.4
|
||||
docker pull ghcr.io/thomiceli/opengist:1.7
|
||||
```
|
||||
|
||||
It can be used in a `docker-compose.yml` file :
|
||||
@@ -71,7 +51,7 @@ version: "3"
|
||||
|
||||
services:
|
||||
opengist:
|
||||
image: ghcr.io/thomiceli/opengist:1.4
|
||||
image: ghcr.io/thomiceli/opengist:1.7
|
||||
container_name: opengist
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -92,9 +72,25 @@ services:
|
||||
GID: 1001
|
||||
```
|
||||
|
||||
### Via binary
|
||||
|
||||
Download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.7.3/opengist1.7.3-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.7.3-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
```
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
### From source
|
||||
|
||||
Requirements : [Git](https://git-scm.com/downloads) (2.20+), [Go](https://go.dev/doc/install) (1.19+), [Node.js](https://nodejs.org/en/download/) (16+)
|
||||
Requirements: [Git](https://git-scm.com/downloads) (2.28+), [Go](https://go.dev/doc/install) (1.22+), [Node.js](https://nodejs.org/en/download/) (16+), [Make](https://linux.die.net/man/1/make) (optional, but easier)
|
||||
|
||||
```shell
|
||||
git clone https://github.com/thomiceli/opengist
|
||||
@@ -105,153 +101,15 @@ make
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
## Configuration
|
||||
---
|
||||
|
||||
Opengist provides flexible configuration options through either a YAML file and/or environment variables.
|
||||
You would only need to specify the configuration options you want to change — for any config option left untouched, Opengist will simply apply the default values.
|
||||
To create and run a development environment, see [run-development.md](/docs/contributing/run-development.md).
|
||||
|
||||
<details>
|
||||
<summary>Configuration option list</summary>
|
||||
## Documentation
|
||||
|
||||
| YAML Config Key | Environment Variable | Default value | Description |
|
||||
|-----------------------|--------------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
||||
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
|
||||
| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. |
|
||||
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
|
||||
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
|
||||
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
|
||||
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
|
||||
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
|
||||
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
|
||||
| http.tls-enabled | OG_HTTP_TLS_ENABLED | `false` | Enable or disable TLS for the HTTP server. (`true` or `false`) |
|
||||
| http.cert-file | OG_HTTP_CERT_FILE | none | Path to the TLS certificate file if TLS is enabled. |
|
||||
| http.key-file | OG_HTTP_KEY_FILE | none | Path to the TLS key file if TLS is enabled. |
|
||||
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
|
||||
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
|
||||
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
|
||||
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
|
||||
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
|
||||
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
|
||||
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
|
||||
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
|
||||
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
|
||||
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
|
||||
The documentation is available in [/docs](/docs) directory.
|
||||
|
||||
</details>
|
||||
|
||||
### Configuration via YAML file
|
||||
|
||||
The configuration file must be specified when launching the application, using the `--config` flag followed by the path to your YAML file.
|
||||
|
||||
```shell
|
||||
./opengist --config /path/to/config.yml
|
||||
```
|
||||
|
||||
You can start by copying and/or modifying the provided [config.yml](config.yml) file.
|
||||
|
||||
### Configuration via Environment Variables
|
||||
|
||||
Usage with Docker Compose :
|
||||
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
environment:
|
||||
OG_LOG_LEVEL: "info"
|
||||
# etc.
|
||||
```
|
||||
Usage via command line :
|
||||
|
||||
```shell
|
||||
OG_LOG_LEVEL=info ./opengist
|
||||
```
|
||||
|
||||
## Administration
|
||||
|
||||
### Use Nginx as a reverse proxy
|
||||
|
||||
Configure Nginx to proxy requests to Opengist. Here is an example configuration file :
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name opengist.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:6157;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run :
|
||||
```shell
|
||||
service nginx restart
|
||||
```
|
||||
|
||||
### Use Fail2ban
|
||||
|
||||
Fail2ban can be used to ban IPs that try to bruteforce the login page.
|
||||
Log level must be set at least to `warn`.
|
||||
|
||||
Add this filter in `etc/fail2ban/filter.d/opengist.conf` :
|
||||
```ini
|
||||
[Definition]
|
||||
failregex = Invalid .* authentication attempt from <HOST>
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
Add this jail in `etc/fail2ban/jail.d/opengist.conf` :
|
||||
```ini
|
||||
[opengist]
|
||||
enabled = true
|
||||
filter = opengist
|
||||
logpath = /home/*/.opengist/log/opengist.log
|
||||
maxretry = 10
|
||||
findtime = 3600
|
||||
bantime = 600
|
||||
banaction = iptables-allports
|
||||
port = anyport
|
||||
```
|
||||
|
||||
Then run
|
||||
```shell
|
||||
service fail2ban restart
|
||||
```
|
||||
|
||||
## Configure OAuth
|
||||
|
||||
Opengist can be configured to use OAuth to authenticate users, with GitHub or Gitea.
|
||||
|
||||
<details>
|
||||
<summary>Integrate Github</summary>
|
||||
|
||||
* Add a new OAuth app in your [Github account settings](https://github.com/settings/applications/new)
|
||||
* Set 'Authorization callback URL' to `http://opengist.domain/oauth/github/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the configuration :
|
||||
```yaml
|
||||
github.client-key: <key>
|
||||
github.secret: <secret>
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Integrate Gitea</summary>
|
||||
|
||||
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
|
||||
* Set 'Redirect URI' to `http://opengist.domain/oauth/gitea/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the configuration :
|
||||
```yaml
|
||||
gitea.client-key: <key>
|
||||
gitea.secret: <secret>
|
||||
# URL of the Gitea instance. Default: https://gitea.com/
|
||||
gitea.url: http://localhost:3000
|
||||
```
|
||||
</details>
|
||||
|
||||
## License
|
||||
|
||||
Opengist is licensed under the [AGPL-3.0 license](LICENSE).
|
||||
Opengist is licensed under the [AGPL-3.0 license](/LICENSE).
|
||||
|
||||
61
config.yml
61
config.yml
@@ -1,8 +1,14 @@
|
||||
# Learn more about Opengist configuration here:
|
||||
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/index.md
|
||||
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md
|
||||
|
||||
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn
|
||||
log-level: warn
|
||||
|
||||
# Public URL for the Git HTTP/SSH connection.
|
||||
# If not set, uses the URL from the request
|
||||
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
|
||||
log-output: stdout,file
|
||||
|
||||
# Public URL to access to Opengist
|
||||
external-url:
|
||||
|
||||
# Directory where Opengist will store its data. Default: ~/.opengist/
|
||||
@@ -11,6 +17,16 @@ opengist-home:
|
||||
# Name of the SQLite database file. Default: opengist.db
|
||||
db-filename: opengist.db
|
||||
|
||||
# Enable or disable the code search index (either `true` or `false`). Default: true
|
||||
index.enabled: true
|
||||
|
||||
# Name of the directory where the code search index is stored. Default: opengist.index
|
||||
index.dirname: opengist.index
|
||||
|
||||
# Default branch name used by Opengist when initializing Git repositories.
|
||||
# If not set, uses the Git default branch name. See https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch
|
||||
git.default-branch:
|
||||
|
||||
# Set the journal mode for SQLite. Default: WAL
|
||||
# See https://www.sqlite.org/pragma.html#pragma_journal_mode
|
||||
sqlite.journal-mode: WAL
|
||||
@@ -26,15 +42,6 @@ http.port: 6157
|
||||
# Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true
|
||||
http.git-enabled: true
|
||||
|
||||
# Enable or disable TLS (either `true` or `false`). Default: false
|
||||
http.tls-enabled: false
|
||||
|
||||
# Path to the TLS certificate file if TLS is enabled
|
||||
http.cert-file:
|
||||
|
||||
# Path to the TLS key file if TLS is enabled
|
||||
http.key-file:
|
||||
|
||||
# SSH built-in server configuration
|
||||
# Note: it is not using the SSH daemon from your machine (yet)
|
||||
|
||||
@@ -60,14 +67,44 @@ ssh.keygen-executable: ssh-keygen
|
||||
|
||||
|
||||
# OAuth2 configuration
|
||||
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitea>/callback
|
||||
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
|
||||
|
||||
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
|
||||
github.client-key:
|
||||
github.secret:
|
||||
|
||||
# To create a new OAuth2 application using Gitlab : https://gitlab.com/-/user_settings/applications
|
||||
gitlab.client-key:
|
||||
gitlab.secret:
|
||||
# URL of the Gitlab instance. Default: https://gitlab.com/
|
||||
gitlab.url: https://gitlab.com/
|
||||
# The name of the GitLab instance. It is displayed in the OAuth login button. Default: GitLab
|
||||
gitlab.name: GitLab
|
||||
|
||||
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
|
||||
gitea.client-key:
|
||||
gitea.secret:
|
||||
# URL of the Gitea instance. Default: https://gitea.com/
|
||||
gitea.url: https://gitea.com/
|
||||
# The name of the Gitea instance. It is displayed in the OAuth login button. Default: Gitea
|
||||
gitea.name: Gitea
|
||||
|
||||
# To create a new OAuth2 application using OpenID Connect:
|
||||
oidc.client-key:
|
||||
oidc.secret:
|
||||
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
||||
oidc.discovery-url:
|
||||
|
||||
|
||||
# Custom assets
|
||||
# Add your own custom assets, that are files relatives to $opengist-home/custom/
|
||||
custom.logo:
|
||||
custom.favicon:
|
||||
|
||||
# Static pages in footer (like legal notices, privacy policy, etc.)
|
||||
# The path can be a URL or a relative path to a file in the $opengist-home/custom/ directory
|
||||
custom.static-links:
|
||||
# - name: Gitea
|
||||
# path: https://gitea.com
|
||||
# - name: Legal notices
|
||||
# path: legal.html
|
||||
|
||||
75
deploy/README.md
Normal file
75
deploy/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# kustomize
|
||||
|
||||
## Simple
|
||||
|
||||
`kustomization.yaml`:
|
||||
|
||||
```yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: opengist
|
||||
|
||||
resources:
|
||||
- https://github.com/thomiceli/opengist/deploy/
|
||||
```
|
||||
|
||||
## Full example
|
||||
|
||||
`kustomization.yaml`:
|
||||
|
||||
```yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: opengist
|
||||
|
||||
namespace: opengist
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- https://github.com/thomiceli/opengist/deploy/?ref:v1.7.3
|
||||
|
||||
images:
|
||||
- name: ghcr.io/thomiceli/opengist
|
||||
newTag: 1.7.3
|
||||
|
||||
patches:
|
||||
# Add your ingress
|
||||
- path: ingress.yaml
|
||||
- patch: |-
|
||||
- op: add
|
||||
path: /spec/rules/0/host
|
||||
value: opengist.mydomain.com
|
||||
target:
|
||||
group: networking.k8s.io
|
||||
version: v1
|
||||
kind: Ingress
|
||||
name: opengist
|
||||
```
|
||||
|
||||
`namespace.yaml`:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: opengist
|
||||
```
|
||||
|
||||
`ingress.yaml`:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: opengist
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-production
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- opengist.mydomain.com
|
||||
secretName: opengist-tls
|
||||
```
|
||||
29
deploy/deployment.yaml
Normal file
29
deploy/deployment.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: opengist
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: opengist
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: opengist
|
||||
spec:
|
||||
containers:
|
||||
- name: opengist
|
||||
image: ghcr.io/thomiceli/opengist
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 6157
|
||||
- name: ssh
|
||||
containerPort: 2222
|
||||
volumeMounts:
|
||||
- mountPath: /opengist
|
||||
name: data
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: opengist-data
|
||||
20
deploy/ingress.yaml
Normal file
20
deploy/ingress.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: opengist
|
||||
labels:
|
||||
app.kubernetes.io/name: opengist
|
||||
app.kubernetes.io/component: ingress
|
||||
spec:
|
||||
rules:
|
||||
- host: opengist.local
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: opengist
|
||||
port:
|
||||
name: http
|
||||
11
deploy/kustomization.yaml
Normal file
11
deploy/kustomization.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: opengist
|
||||
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- pvc.yaml
|
||||
- ingress.yaml
|
||||
- service.yaml
|
||||
15
deploy/pvc.yaml
Normal file
15
deploy/pvc.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: opengist-data
|
||||
labels:
|
||||
app.kubernetes.io/name: opengist
|
||||
app.kubernetes.io/component: data
|
||||
spec:
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
14
deploy/service.yaml
Normal file
14
deploy/service.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: opengist
|
||||
labels:
|
||||
app.kubernetes.io/name: opengist
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: opengist
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
name: http
|
||||
@@ -7,5 +7,6 @@ groupmod -o -g "$GID" $USER
|
||||
usermod -o -u "$UID" $USER
|
||||
|
||||
chown -R "$USER:$USER" /opengist
|
||||
chown -R "$USER:$USER" /config.yml
|
||||
|
||||
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist"
|
||||
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml"
|
||||
|
||||
29
docs/administration/fail2ban-setup.md
Normal file
29
docs/administration/fail2ban-setup.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Fail2ban setup
|
||||
|
||||
Fail2ban can be used to ban IPs that try to bruteforce the login page.
|
||||
Log level must be set at least to `warn`.
|
||||
|
||||
Add this filter in `etc/fail2ban/filter.d/opengist.conf` :
|
||||
```ini
|
||||
[Definition]
|
||||
failregex = Invalid .* authentication attempt from <HOST>
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
Add this jail in `etc/fail2ban/jail.d/opengist.conf` :
|
||||
```ini
|
||||
[opengist]
|
||||
enabled = true
|
||||
filter = opengist
|
||||
logpath = /home/*/.opengist/log/opengist.log
|
||||
maxretry = 10
|
||||
findtime = 3600
|
||||
bantime = 600
|
||||
banaction = iptables-allports
|
||||
port = anyport
|
||||
```
|
||||
|
||||
Then run
|
||||
```shell
|
||||
service fail2ban restart
|
||||
```
|
||||
13
docs/administration/healthcheck.md
Normal file
13
docs/administration/healthcheck.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Healthcheck
|
||||
|
||||
A healthcheck is a simple HTTP GET request to the `/healthcheck` endpoint. It returns a `200 OK` response if the server is healthy.
|
||||
|
||||
## Example
|
||||
|
||||
```shell
|
||||
curl http://localhost:6157/healthcheck
|
||||
```
|
||||
|
||||
```json
|
||||
{"database":"ok","opengist":"ok","time":"2024-01-04T05:18:33+01:00"}
|
||||
```
|
||||
46
docs/administration/nginx-reverse-proxy.md
Normal file
46
docs/administration/nginx-reverse-proxy.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Use Nginx as a reverse proxy
|
||||
|
||||
Configure Nginx to proxy requests to Opengist. Here are example configuration file to use Opengist on a subdomain or on a subpath.
|
||||
|
||||
Make sure you set the base url for Opengist via the [configuration](/docs/configuration/cheat-sheet.md).
|
||||
|
||||
### Subdomain
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name opengist.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:6157;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Subpath
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
|
||||
location /opengist/ {
|
||||
rewrite ^/opengist(/.*)$ $1 break;
|
||||
proxy_pass http://127.0.0.1:6157;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Prefix /opengist;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To apply changes:
|
||||
```shell
|
||||
sudo systemctl restart nginx
|
||||
```
|
||||
52
docs/administration/oauth-providers.md
Normal file
52
docs/administration/oauth-providers.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Use OAuth providers
|
||||
|
||||
Opengist can be configured to use OAuth to authenticate users, with GitHub, Gitea, or OpenID Connect.
|
||||
|
||||
## Github
|
||||
|
||||
* Add a new OAuth app in your [GitHub account settings](https://github.com/settings/applications/new)
|
||||
* Set 'Authorization callback URL' to `http://opengist.url/oauth/github/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
||||
```yaml
|
||||
github.client-key: <key>
|
||||
github.secret: <secret>
|
||||
```
|
||||
|
||||
|
||||
## GitLab
|
||||
|
||||
* Add a new OAuth app in Application settings from the [GitLab instance](https://gitlab.com/-/user_settings/applications)
|
||||
* Set 'Redirect URI' to `http://opengist.url/oauth/gitlab/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
||||
```yaml
|
||||
gitlab.client-key: <key>
|
||||
gitlab.secret: <secret>
|
||||
# URL of the GitLab instance. Default: https://gitlab.com/
|
||||
gitlab.url: https://gitlab.com/
|
||||
```
|
||||
|
||||
|
||||
## Gitea
|
||||
|
||||
* Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications)
|
||||
* Set 'Redirect URI' to `http://opengist.url/oauth/gitea/callback`
|
||||
* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
||||
```yaml
|
||||
gitea.client-key: <key>
|
||||
gitea.secret: <secret>
|
||||
# URL of the Gitea instance. Default: https://gitea.com/
|
||||
gitea.url: http://localhost:3000
|
||||
```
|
||||
|
||||
|
||||
## OpenID Connect
|
||||
|
||||
* Add a new OAuth app in Application settings of your OIDC provider
|
||||
* Set 'Redirect URI' to `http://opengist.url/oauth/openid-connect/callback`
|
||||
* Copy the 'Client ID', 'Client Secret', and the discovery endpoint, and add them to the [configuration](/docs/configuration/cheat-sheet.md) :
|
||||
```yaml
|
||||
oidc.client-key: <key>
|
||||
oidc.secret: <secret>
|
||||
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
|
||||
oidc.discovery-url: http://auth.example.com/.well-known/openid-configuration
|
||||
```
|
||||
7
docs/administration/reset-password.md
Normal file
7
docs/administration/reset-password.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Reset a user password
|
||||
|
||||
To reset a user password, run the following command using the Opengist binary:
|
||||
|
||||
```bash
|
||||
./opengist admin reset-password <username> <new-password>
|
||||
```
|
||||
92
docs/administration/run-with-systemd.md
Normal file
92
docs/administration/run-with-systemd.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Run with Systemd
|
||||
|
||||
For non-Docker users, you could run Opengist as a systemd service.
|
||||
|
||||
## As root
|
||||
On Unix distributions with systemd, place the Opengist binary like:
|
||||
|
||||
```shell
|
||||
sudo cp opengist /usr/local/bin
|
||||
sudo mkdir -p /var/lib/opengist
|
||||
sudo cp config.yml /etc/opengist
|
||||
```
|
||||
|
||||
Edit the Opengist home directory configuration in `/etc/opengist/config.yml` like:
|
||||
```shell
|
||||
opengist-home: /var/lib/opengist
|
||||
```
|
||||
|
||||
Create a new user to run Opengist:
|
||||
```shell
|
||||
sudo useradd --system opengist
|
||||
sudo mkdir -p /var/lib/opengist
|
||||
sudo chown -R opengist:opengist /var/lib/opengist
|
||||
```
|
||||
|
||||
Then create a service file at `/etc/systemd/system/opengist.service`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=opengist Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=opengist
|
||||
Group=opengist
|
||||
ExecStart=opengist --config /etc/opengist/config.yml
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Finally, start the service:
|
||||
```shell
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now opengist
|
||||
systemctl status opengist
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
## As a normal user
|
||||
**NOTE: This was tested on Ubuntu 20.04 and newer. For other distros, please check the respective documentation**
|
||||
|
||||
#### For the purpose of this documentation, we will assume that:
|
||||
- You've followed the instructions on how to run opengist [from source](https://github.com/thomiceli/opengist?tab=readme-ov-file#from-source)
|
||||
- Your shell user is named `pastebin`
|
||||
- All commands are being executed as the `pastebin` user
|
||||
|
||||
_If none of the above is true, then adapt the commands and paths to fit your needs._
|
||||
|
||||
Enable lingering for the user:
|
||||
```shell
|
||||
loginctl enable-linger
|
||||
```
|
||||
|
||||
Create the user systemd folder:
|
||||
```
|
||||
mkdir -p /home/pastebin/.config/systemd/user
|
||||
```
|
||||
|
||||
Then create a service file at `/home/pastebin/.config/systemd/user/opengist.service`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=opengist Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/home/pastebin/opengist/opengist --config /home/pastebin/opengist/config.yml
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Finally, start the service:
|
||||
```shell
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now opengist
|
||||
systemctl --user status opengist
|
||||
```
|
||||
37
docs/configuration/cheat-sheet.md
Normal file
37
docs/configuration/cheat-sheet.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Configuration Cheat Sheet
|
||||
|
||||
| YAML Config Key | Environment Variable | Default value | Description |
|
||||
|-----------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
|
||||
| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. |
|
||||
| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. |
|
||||
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
|
||||
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
|
||||
| index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) |
|
||||
| index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. |
|
||||
| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) |
|
||||
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
|
||||
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
|
||||
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
|
||||
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
|
||||
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
|
||||
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
|
||||
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
|
||||
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
|
||||
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
|
||||
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
|
||||
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
|
||||
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
|
||||
| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. |
|
||||
| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. |
|
||||
| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. |
|
||||
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
|
||||
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
|
||||
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
|
||||
| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. |
|
||||
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
|
||||
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
|
||||
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
|
||||
| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. |
|
||||
| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. |
|
||||
| custom.static-links | OG_CUSTOM_STATIC_LINK_#_(PATH,NAME) | none | Path and name to custom links, more info [here](custom-links.md). |
|
||||
31
docs/configuration/custom-assets.md
Normal file
31
docs/configuration/custom-assets.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Custom assets
|
||||
|
||||
To add custom assets to your Opengist instance, you can use the `$opengist-home/custom` directory (where `$opengist-home` is the directory where Opengist stores its data).
|
||||
|
||||
### Logo / Favicon
|
||||
|
||||
To add a custom logo or favicon, you can add your own image file to the `$opengist-home/custom` directory, then define the relative path in the config.
|
||||
|
||||
For example, if you have a logo file `logo.png` in the `$opengist-home/custom` directory, you can set the logo path in the config as follows:
|
||||
|
||||
#### YAML
|
||||
```yaml
|
||||
custom.logo: logo.png
|
||||
```
|
||||
|
||||
#### Environment variable
|
||||
```sh
|
||||
export OG_CUSTOM_LOGO=logo.png
|
||||
```
|
||||
|
||||
Same as the favicon:
|
||||
|
||||
#### YAML
|
||||
```yaml
|
||||
custom.favicon: favicon.png
|
||||
```
|
||||
|
||||
#### Environment variable
|
||||
```sh
|
||||
export OG_CUSTOM_FAVICON=favicon.png
|
||||
```
|
||||
62
docs/configuration/custom-links.md
Normal file
62
docs/configuration/custom-links.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Custom links
|
||||
|
||||
On the footer of your Opengist instance, you can add links to custom static templates or any other website you want to link to.
|
||||
This can be useful for legal information, privacy policy, or any other information you want to provide to your users.
|
||||
|
||||
To add one or more links, you can add your own file to the `$opengist-home/custom` directory or set a URL, then define the relative path and its name in the config.
|
||||
|
||||
For example, if you have a legal information file `legal.html` in the `$opengist-home/custom` directory, and also wish to add a link to a Gitea instance, you can set the link in the config as follows:
|
||||
|
||||
#### YAML
|
||||
```yaml
|
||||
custom.static-links:
|
||||
- name: Legal notices
|
||||
path: legal.html
|
||||
- name: Gitea
|
||||
path: https://gitea.com
|
||||
```
|
||||
|
||||
#### Environment variable
|
||||
```sh
|
||||
OG_CUSTOM_STATIC_LINK_0_NAME="Legal Notices" \
|
||||
OG_CUSTOM_STATIC_LINK_0_PATH=legal.html \
|
||||
OG_CUSTOM_STATIC_LINK_1_NAME=Gitea \
|
||||
OG_CUSTOM_STATIC_LINK_1_PATH=https://gitea.com \
|
||||
./opengist
|
||||
```
|
||||
|
||||
## Templating custom HTML pages
|
||||
|
||||
In the start and end of the custom HTML files, you can use the syntax to include the header and footer of the Opengist instance:
|
||||
|
||||
```html
|
||||
{{ template "header" . }}
|
||||
|
||||
<!-- my content -->
|
||||
|
||||
{{ template "footer" . }}
|
||||
```
|
||||
|
||||
If you want your custom page to integrate well into the existing theme, you can use the following:
|
||||
|
||||
```html
|
||||
{{ template "header" . }}
|
||||
|
||||
<div class="py-10">
|
||||
<header class="pb-4 ">
|
||||
<div class="flex">
|
||||
<div class="flex-auto">
|
||||
<h2 class="text-2xl font-bold leading-tight">Heading</h2>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<h3 class="text-xl font-bold leading-tight mt-4">Sub-Heading</h3>
|
||||
<p class="mt-4 ml-1"><!-- content --></p>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{{ template "footer" . }}
|
||||
```
|
||||
|
||||
You can adjust above as needed. Opengist uses Tailwind CSS classes.
|
||||
48
docs/configuration/index.md
Normal file
48
docs/configuration/index.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Configuration
|
||||
|
||||
Opengist provides flexible configuration options through either a YAML file and/or environment variables.
|
||||
You would only need to specify the configuration options you want to change — for any config option left untouched,
|
||||
Opengist will simply apply the default values.
|
||||
|
||||
The [configuration cheat sheet](cheat-sheet.md) lists all available configuration options.
|
||||
|
||||
|
||||
## Configuration via YAML file
|
||||
|
||||
The configuration file must be specified when launching the application, using the `--config` flag followed by the path
|
||||
to your YAML file.
|
||||
|
||||
Usage with Docker Compose :
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
volumes:
|
||||
# ...
|
||||
- "/path/to/config.yml:/config.yml"
|
||||
```
|
||||
|
||||
Usage via command line :
|
||||
```shell
|
||||
./opengist --config /path/to/config.yml
|
||||
```
|
||||
|
||||
You can start by copying and/or modifying the provided [config.yml](/config.yml) file.
|
||||
|
||||
|
||||
## Configuration via Environment Variables
|
||||
|
||||
Usage with Docker Compose :
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
environment:
|
||||
OG_LOG_LEVEL: "info"
|
||||
# etc.
|
||||
```
|
||||
|
||||
Usage via command line :
|
||||
```shell
|
||||
OG_LOG_LEVEL=info ./opengist
|
||||
```
|
||||
38
docs/contributing/run-development.md
Normal file
38
docs/contributing/run-development.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Run Opengist in development mode
|
||||
|
||||
## With Docker
|
||||
|
||||
Assuming you have [Make](https://linux.die.net/man/1/make) installed,
|
||||
|
||||
```shell
|
||||
# Clone the repository
|
||||
git clone git@github.com:thomiceli/opengist.git
|
||||
cd opengist
|
||||
|
||||
# Build the development image
|
||||
make build_dev_docker
|
||||
```
|
||||
|
||||
Now you can run the development image with the following command:
|
||||
|
||||
```shell
|
||||
make run_dev_docker
|
||||
```
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
## As a binary
|
||||
|
||||
Requirements:
|
||||
* [Git](https://git-scm.com/downloads) (2.28+)
|
||||
* [Go](https://go.dev/doc/install) (1.22+)
|
||||
* [Node.js](https://nodejs.org/en/download/) (16+)
|
||||
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
|
||||
|
||||
```shell
|
||||
git clone git@github.com:thomiceli/opengist.git
|
||||
cd opengist
|
||||
make watch
|
||||
```
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
54
docs/index.md
Normal file
54
docs/index.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Opengist
|
||||
|
||||
Opengist is a **self-hosted** pastebin **powered by Git**. All snippets are stored in a Git repository and can be
|
||||
read and/or modified using standard Git commands, or with the web interface.
|
||||
It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and could be self-hosted.
|
||||
|
||||
Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Create public, unlisted or private snippets
|
||||
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||
* Syntax highlighting ; markdown & CSV support
|
||||
* Search code in snippets ; browse users snippets, likes and forks
|
||||
* Embed snippets in other websites
|
||||
* Revisions history
|
||||
* Like / Fork snippets
|
||||
* Editor with indentation mode & size ; drag and drop files
|
||||
* Download raw files or as a ZIP archive
|
||||
* Retrieve snippet data/metadata via a JSON API
|
||||
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
|
||||
* Avatars via Gravatar or OAuth2 providers
|
||||
* Light/Dark mode
|
||||
* Responsive UI
|
||||
* Enable or disable signups
|
||||
* Restrict or unrestrict snippets visibility to anonymous users
|
||||
* Admin panel :
|
||||
* delete users/gists;
|
||||
* clean database/filesystem by syncing gists
|
||||
* run `git gc` for all repositories
|
||||
* SQLite database
|
||||
* Logging
|
||||
* Docker support
|
||||
|
||||
|
||||
## System requirements
|
||||
|
||||
[Git](https://git-scm.com/download) is obviously required to run Opengist, as it's the main feature of the app.
|
||||
Version **2.28** or later is recommended as the app has not been tested with older Git versions and some features would not work.
|
||||
|
||||
[OpenSSH](https://www.openssh.com/) suite if you wish to use Git over SSH.
|
||||
|
||||
|
||||
## Components
|
||||
|
||||
* Backend Web Framework: [Echo](https://echo.labstack.com/)
|
||||
* ORM: [GORM](https://gorm.io/)
|
||||
* Frontend libraries:
|
||||
* [Tailwind CSS](https://tailwindcss.com/)
|
||||
* [CodeMirror](https://codemirror.net/)
|
||||
* [Day.js](https://day.js.org/)
|
||||
* [highlight.js](https://highlightjs.org/)
|
||||
* and [others](/package.json)
|
||||
74
docs/installation.md
Normal file
74
docs/installation.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Installation
|
||||
|
||||
## With Docker
|
||||
|
||||
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/thomiceli/opengist:1
|
||||
```
|
||||
|
||||
It can be used in a `docker-compose.yml` file :
|
||||
|
||||
1. Create a `docker-compose.yml` file with the following content
|
||||
2. Run `docker compose up -d`
|
||||
3. Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
|
||||
```yml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
opengist:
|
||||
image: ghcr.io/thomiceli/opengist:1
|
||||
container_name: opengist
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6157:6157" # HTTP port
|
||||
- "2222:2222" # SSH port, can be removed if you don't use SSH
|
||||
volumes:
|
||||
- "$HOME/.opengist:/opengist"
|
||||
```
|
||||
|
||||
You can define which user/group should run the container and own the files by setting the `UID` and `GID` environment
|
||||
variables :
|
||||
|
||||
```yml
|
||||
services:
|
||||
opengist:
|
||||
# ...
|
||||
environment:
|
||||
UID: 1001
|
||||
GID: 1001
|
||||
```
|
||||
|
||||
## Via binary
|
||||
|
||||
Download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.7.3/opengist1.7.3-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.7.3-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
```
|
||||
|
||||
|
||||
## From source
|
||||
|
||||
Requirements:
|
||||
* [Git](https://git-scm.com/downloads) (2.28+)
|
||||
* [Go](https://go.dev/doc/install) (1.22+)
|
||||
* [Node.js](https://nodejs.org/en/download/) (16+)
|
||||
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
|
||||
|
||||
```shell
|
||||
git clone https://github.com/thomiceli/opengist
|
||||
cd opengist
|
||||
make
|
||||
./opengist
|
||||
```
|
||||
|
||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
||||
57
docs/update.md
Normal file
57
docs/update.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Update
|
||||
|
||||
## Make a backup
|
||||
|
||||
Before updating, always make sure to backup the Opengist home directory, where all the data is stored.
|
||||
|
||||
You can do so by copying the `~/.opengist` directory (default location).
|
||||
|
||||
```shell
|
||||
cp -r ~/.opengist ~/.opengist.bak
|
||||
```
|
||||
|
||||
## Install the new version
|
||||
|
||||
### With Docker
|
||||
|
||||
Pull the last version of Opengist
|
||||
```shell
|
||||
docker pull ghcr.io/thomiceli/opengist:1
|
||||
```
|
||||
|
||||
And restart the container, using `docker compose up -d` for example if you use docker compose.
|
||||
|
||||
### Via binary
|
||||
|
||||
Stop the running instance; then like your first installation of Opengist, download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
|
||||
|
||||
```shell
|
||||
# example for linux amd64
|
||||
wget https://github.com/thomiceli/opengist/releases/download/v1.7.3/opengist1.7.3-linux-amd64.tar.gz
|
||||
|
||||
tar xzvf opengist1.7.3-linux-amd64.tar.gz
|
||||
cd opengist
|
||||
chmod +x opengist
|
||||
./opengist # with or without `--config config.yml`
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
Stop the running instance; then pull the last changes from the master branch, and build the new version.
|
||||
|
||||
```shell
|
||||
git pull
|
||||
make
|
||||
./opengist
|
||||
```
|
||||
|
||||
## Restore the backup
|
||||
|
||||
If you have any issue with the new version, you can restore the backup you made before updating.
|
||||
|
||||
```shell
|
||||
rm -rf ~/.opengist
|
||||
cp -r ~/.opengist.bak ~/.opengist
|
||||
```
|
||||
|
||||
Then run the old version of Opengist again.
|
||||
11
docs/usage/embed.md
Normal file
11
docs/usage/embed.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Embed a Gist to your webpage
|
||||
|
||||
To embed a Gist to your webpage, you can add a script tag with the URL of your gist followed by `.js` to your HTML page:
|
||||
|
||||
```html
|
||||
<script src="http://opengist.url/user/gist-url.js"></script>
|
||||
|
||||
<!-- Dark mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?dark"></script>
|
||||
```
|
||||
|
||||
37
docs/usage/gist-json.md
Normal file
37
docs/usage/gist-json.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Retrieve Gist as JSON
|
||||
|
||||
To retrieve a Gist as JSON, you can add `.json` to the end of the URL of your gist:
|
||||
|
||||
```shell
|
||||
curl http://opengist.url/thomas/my-gist.json | jq '.'
|
||||
```
|
||||
|
||||
It returns a JSON object with the following structure similar to this one:
|
||||
```json
|
||||
{
|
||||
"created_at": "2023-04-12T13:15:20+02:00",
|
||||
"description": "",
|
||||
"embed": {
|
||||
"css": "http://localhost:6157/assets/embed-94abc261.css",
|
||||
"html": "<div class=\"opengist-embed\" id=\"my-gist\">\n <div class=\"html \">\n \n <div class=\"rounded-md border-1 border-gray-100 dark:border-gray-800 overflow-auto mb-4\">\n <div class=\"border-b-1 border-gray-100 dark:border-gray-700 text-xs p-2 pl-4 bg-gray-50 dark:bg-gray-800 text-gray-400\">\n <a target=\"_blank\" href=\"http://localhost:6157/thomas/my-gist#file-hello-md\"><span class=\"font-bold text-gray-700 dark:text-gray-200\">hello.md</span> · 21 B · Markdown</a>\n <span class=\"float-right\"><a target=\"_blank\" href=\"http://localhost:6157\">Hosted via Opengist</a> · <span class=\"text-gray-700 dark:text-gray-200 font-bold\"><a target=\"_blank\" href=\"http://localhost:6157/thomas/my-gist/raw/HEAD/hello.md\">view raw</a></span></span>\n </div>\n \n \n \n <div class=\"chroma markdown markdown-body p-8\"><h1>Welcome to Opengist</h1>\n</div>\n \n\n </div>\n \n </div>\n</div>\n",
|
||||
"js": "http://localhost:6157/thomas/my-gist.js",
|
||||
"js_dark": "http://localhost:6157/thomas/my-gist.js?dark"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"filename": "hello.md",
|
||||
"size": 21,
|
||||
"human_size": "21 B",
|
||||
"content": "# Welcome to Opengist",
|
||||
"truncated": false,
|
||||
"type": "Markdown"
|
||||
}
|
||||
],
|
||||
"id": "my-gist",
|
||||
"owner": "thomas",
|
||||
"title": "hello.md",
|
||||
"uuid": "8622b297bce54b408e36d546cef8019d",
|
||||
"visibility": "public"
|
||||
}
|
||||
```
|
||||
|
||||
26
docs/usage/git-push-options.md
Normal file
26
docs/usage/git-push-options.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Push Options
|
||||
|
||||
Opengist has support for a few [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt).
|
||||
|
||||
These options are passed to `git push` command and can be used to change the metadata of a gist.
|
||||
|
||||
## Set URL
|
||||
|
||||
```shell
|
||||
git push -o url=mygist # Will set the URL to https://opengist.example.com/user/mygist
|
||||
```
|
||||
|
||||
## Change title
|
||||
|
||||
```shell
|
||||
git push -o title=Gist123
|
||||
git push -o title="My Gist 123"
|
||||
```
|
||||
|
||||
## Change visibility
|
||||
|
||||
```shell
|
||||
git push -o visibility=public
|
||||
git push -o visibility=unlisted
|
||||
git push -o visibility=private
|
||||
```
|
||||
23
docs/usage/import-from-github-gist.md
Normal file
23
docs/usage/import-from-github-gist.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Import Gists from GitHub
|
||||
|
||||
After running Opengist at least once, you can import your Gists from GitHub using this script:
|
||||
|
||||
```shell
|
||||
github_user=user # replace with your GitHub username
|
||||
opengist_url="http://user:password@opengist.url/init" # replace user, password and Opengist url
|
||||
|
||||
curl -s https://api.github.com/users/"$github_user"/gists?per_page=100 | jq '.[] | .git_pull_url' -r | while read url; do
|
||||
git clone "$url"
|
||||
repo_dir=$(basename "$url" .git)
|
||||
|
||||
# Add remote, push, and remove the directory
|
||||
if [ -d "$repo_dir" ]; then
|
||||
cd "$repo_dir"
|
||||
git remote add gist "$opengist_url"
|
||||
git push -u gist --all
|
||||
cd ..
|
||||
rm -rf "$repo_dir"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
42
docs/usage/init-via-git.md
Normal file
42
docs/usage/init-via-git.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Init Gists via Git
|
||||
|
||||
Opengist allows you to create new snippets via Git over HTTP.
|
||||
|
||||
Simply init a new Git repository where your file(s) is/are located:
|
||||
|
||||
```shell
|
||||
git init
|
||||
git add .
|
||||
git commit -m "My cool snippet"
|
||||
```
|
||||
|
||||
Then add this Opengist special remote URL and push your changes:
|
||||
|
||||
```shell
|
||||
git remote add origin http://localhost:6157/init
|
||||
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
Log in with your Opengist account credentials, and your snippet will be created at the specified URL:
|
||||
|
||||
```shell
|
||||
Username for 'http://localhost:6157': thomas
|
||||
Password for 'http://thomas@localhost:6157':
|
||||
Enumerating objects: 3, done.
|
||||
Counting objects: 100% (3/3), done.
|
||||
Delta compression using up to 8 threads
|
||||
Compressing objects: 100% (2/2), done.
|
||||
Writing objects: 100% (3/3), 416 bytes | 416.00 KiB/s, done.
|
||||
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||
remote:
|
||||
remote: Your new repository has been created here: http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066
|
||||
remote:
|
||||
remote: If you want to keep working with your gist, you could set the remote URL via:
|
||||
remote: git remote set-url origin http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066
|
||||
remote:
|
||||
To http://localhost:6157/init
|
||||
* [new branch] master -> master
|
||||
```
|
||||
|
||||
https://github.com/thomiceli/opengist/assets/27960254/3fe1a0ba-b638-4928-83a1-f38e46fea066
|
||||
@@ -1,8 +0,0 @@
|
||||
//go:build fs_embed
|
||||
|
||||
package main
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed templates/*/*.html public/manifest.json public/assets/*.js public/assets/*.css public/assets/*.svg public/assets/*.png
|
||||
var dirFS embed.FS
|
||||
7
fs_os.go
7
fs_os.go
@@ -1,7 +0,0 @@
|
||||
//go:build !fs_embed
|
||||
|
||||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
var dirFS = os.DirFS(".")
|
||||
107
go.mod
107
go.mod
@@ -1,42 +1,95 @@
|
||||
module github.com/thomiceli/opengist
|
||||
|
||||
go 1.19
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.11.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/labstack/echo/v4 v4.10.0
|
||||
github.com/markbates/goth v1.77.0
|
||||
github.com/mattn/go-sqlite3 v1.14.13
|
||||
github.com/rs/zerolog v1.29.0
|
||||
golang.org/x/crypto v0.2.0
|
||||
golang.org/x/text v0.7.0
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0
|
||||
github.com/alecthomas/chroma/v2 v2.14.0
|
||||
github.com/blevesearch/bleve/v2 v2.4.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/glebarez/go-sqlite v1.22.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-playground/validator/v10 v10.21.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/hashicorp/go-memdb v1.3.4
|
||||
github.com/labstack/echo/v4 v4.12.0
|
||||
github.com/markbates/goth v1.80.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/urfave/cli/v2 v2.27.2
|
||||
github.com/yuin/goldmark v1.7.1
|
||||
github.com/yuin/goldmark-emoji v1.0.2
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.abhg.dev/goldmark/mermaid v0.5.0
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/text v0.15.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/sqlite v1.3.2
|
||||
gorm.io/gorm v1.23.5
|
||||
gorm.io/gorm v1.25.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.9.4 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.1.8 // indirect
|
||||
github.com/blevesearch/geo v0.1.20 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.16 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.13 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||
github.com/blevesearch/vellum v1.0.10 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.3.10 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.3.10 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.3.10 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.3.10 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.3.13 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.1.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/mux v1.6.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/time v0.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/oauth2 v0.20.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
modernc.org/libc v1.51.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.30.0 // indirect
|
||||
)
|
||||
|
||||
696
go.sum
696
go.sum
@@ -1,498 +1,264 @@
|
||||
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=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
|
||||
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
|
||||
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
|
||||
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blevesearch/bleve/v2 v2.4.0 h1:2xyg+Wv60CFHYccXc+moGxbL+8QKT/dZK09AewHgKsg=
|
||||
github.com/blevesearch/bleve/v2 v2.4.0/go.mod h1:IhQHoFAbHgWKYavb9rQgQEJJVMuY99cKdQ0wPpst2aY=
|
||||
github.com/blevesearch/bleve_index_api v1.1.8 h1:rJUccYfWqRY2/BGowlsv1lwrLKYK/zPE6hgNn1pTGdk=
|
||||
github.com/blevesearch/bleve_index_api v1.1.8/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
|
||||
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
||||
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
||||
github.com/blevesearch/go-faiss v1.0.16 h1:lfzXzzjO1mAf15MRiRY5yz6KVGr02CyRrr7m0z70Ih8=
|
||||
github.com/blevesearch/go-faiss v1.0.16/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.13 h1:UfbyRpIMdcaNsgciGYS9Pib7N3xd3EEw8KKbd/aDBlA=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.2.13/go.mod h1:osG1bAUONZB2r/ozUJwjbuOzPvdrULWaLOm+vsMANsk=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
|
||||
github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=
|
||||
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
|
||||
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
|
||||
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
|
||||
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
|
||||
github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=
|
||||
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=
|
||||
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
|
||||
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
|
||||
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
|
||||
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
|
||||
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
|
||||
github.com/blevesearch/zapx/v16 v16.1.0 h1:bHsyowFqU0QA+uVDJCjifv9OvPGb8htkV52Yc/wT6xs=
|
||||
github.com/blevesearch/zapx/v16 v16.1.0/go.mod h1:P0h9lKRyl4EKksAWfxwCQ5I5pLB9jH2XD8bhYHuIYuc=
|
||||
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY=
|
||||
github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA=
|
||||
github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ=
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0=
|
||||
github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
||||
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
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.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
|
||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
|
||||
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
|
||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA=
|
||||
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
||||
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/markbates/goth v1.77.0 h1:s3scqnWv/Zq/a5M766V0FKsLfOdFNdh/HEkuWCKbvT8=
|
||||
github.com/markbates/goth v1.77.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
|
||||
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
||||
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
|
||||
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
||||
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
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-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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-20190108225652-1e06a53dbb7e/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
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/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW38=
|
||||
go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
|
||||
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-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-20190418145605-e7d98fc518a7/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-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
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/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
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=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg=
|
||||
gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
|
||||
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
|
||||
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/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=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
|
||||
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.17.8 h1:yyWBf2ipA0Y9GGz/MmCmi3EFpKgeS7ICrAFes+suEbs=
|
||||
modernc.org/ccgo/v4 v4.17.8/go.mod h1:buJnJ6Fn0tyAdP/dqePbrrvLyr6qslFfTbFrCuaYvtA=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/libc v1.51.0 h1:kjSHjz1guHbI5iRdi6nEr/wIKSN6X4vzLd6TJMN+lHA=
|
||||
modernc.org/libc v1.51.0/go.mod h1:15P6ublJ9FJR8YQCGy8DeQ2Uwur7iW9Hserr/T3OFZE=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.30.0 h1:8YhPUs/HTnlEgErn/jSYQTwHN/ex8CjHHjg+K9iG7LM=
|
||||
modernc.org/sqlite v1.30.0/go.mod h1:cgkTARJ9ugeXSNaLBPK3CqbOe7Ec7ZhWPoMFGldEYEw=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
177
internal/actions/actions.go
Normal file
177
internal/actions/actions.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ActionStatus struct {
|
||||
Running bool
|
||||
}
|
||||
|
||||
const (
|
||||
SyncReposFromFS = iota
|
||||
SyncReposFromDB
|
||||
GitGcRepos
|
||||
SyncGistPreviews
|
||||
ResetHooks
|
||||
IndexGists
|
||||
)
|
||||
|
||||
var (
|
||||
mutex sync.Mutex
|
||||
actions = make(map[int]ActionStatus)
|
||||
)
|
||||
|
||||
func updateActionStatus(actionType int, running bool) {
|
||||
actions[actionType] = ActionStatus{
|
||||
Running: running,
|
||||
}
|
||||
}
|
||||
|
||||
func IsRunning(actionType int) bool {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
return actions[actionType].Running
|
||||
}
|
||||
|
||||
func Run(actionType int) {
|
||||
mutex.Lock()
|
||||
|
||||
if actions[actionType].Running {
|
||||
mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
updateActionStatus(actionType, true)
|
||||
mutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
mutex.Lock()
|
||||
updateActionStatus(actionType, false)
|
||||
mutex.Unlock()
|
||||
}()
|
||||
|
||||
var functionToRun func()
|
||||
switch actionType {
|
||||
case SyncReposFromFS:
|
||||
functionToRun = syncReposFromFS
|
||||
case SyncReposFromDB:
|
||||
functionToRun = syncReposFromDB
|
||||
case GitGcRepos:
|
||||
functionToRun = gitGcRepos
|
||||
case SyncGistPreviews:
|
||||
functionToRun = syncGistPreviews
|
||||
case ResetHooks:
|
||||
functionToRun = resetHooks
|
||||
case IndexGists:
|
||||
functionToRun = indexGists
|
||||
default:
|
||||
log.Error().Msg("Unknown action type")
|
||||
}
|
||||
|
||||
functionToRun()
|
||||
}
|
||||
|
||||
func syncReposFromFS() {
|
||||
log.Info().Msg("Syncing repositories from filesystem...")
|
||||
gists, err := db.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
return
|
||||
}
|
||||
for _, gist := range gists {
|
||||
// if repository does not exist, delete gist from database
|
||||
if _, err := os.Stat(git.RepositoryPath(gist.User.Username, gist.Uuid)); err != nil && !os.IsExist(err) {
|
||||
if err2 := gist.Delete(); err2 != nil {
|
||||
log.Error().Err(err2).Msgf("Cannot delete gist %d", gist.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncReposFromDB() {
|
||||
log.Info().Msg("Syncing repositories from database...")
|
||||
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot read repos directories")
|
||||
return
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
path := strings.Split(e, string(os.PathSeparator))
|
||||
gist, _ := db.GetGist(path[len(path)-2], path[len(path)-1])
|
||||
|
||||
if gist.ID == 0 {
|
||||
if err := git.DeleteRepository(path[len(path)-2], path[len(path)-1]); err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot delete repository %s/%s", path[len(path)-2], path[len(path)-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func gitGcRepos() {
|
||||
log.Info().Msg("Garbage collecting all repositories...")
|
||||
if err := git.GcRepos(); err != nil {
|
||||
log.Error().Err(err).Msg("Error garbage collecting repositories")
|
||||
}
|
||||
}
|
||||
|
||||
func syncGistPreviews() {
|
||||
log.Info().Msg("Syncing all Gist previews...")
|
||||
|
||||
gists, err := db.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
return
|
||||
}
|
||||
for _, gist := range gists {
|
||||
if err = gist.UpdatePreviewAndCount(false); err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot update preview and count for gist %d", gist.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resetHooks() {
|
||||
log.Info().Msg("Resetting Git server hooks for all repositories...")
|
||||
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot read repos directories")
|
||||
return
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
path := strings.Split(e, string(os.PathSeparator))
|
||||
if err := git.CreateDotGitFiles(path[len(path)-2], path[len(path)-1]); err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot reset hooks for repository %s/%s", path[len(path)-2], path[len(path)-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func indexGists() {
|
||||
log.Info().Msg("Indexing all Gists...")
|
||||
gists, err := db.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
return
|
||||
}
|
||||
|
||||
for _, gist := range gists {
|
||||
log.Info().Msgf("Indexing gist %d", gist.ID)
|
||||
indexedGist, err := gist.ToIndexedGist()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
|
||||
continue
|
||||
}
|
||||
if err = index.AddInIndex(indexedGist); err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot index gist %d", gist.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
18
internal/auth/auth.go
Normal file
18
internal/auth/auth.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package auth
|
||||
|
||||
type AuthInfoProvider interface {
|
||||
RequireLogin() (bool, error)
|
||||
AllowGistsWithoutLogin() (bool, error)
|
||||
}
|
||||
|
||||
func ShouldAllowUnauthenticatedGistAccess(prov AuthInfoProvider, isSingleGistAccess bool) (bool, error) {
|
||||
require, err := prov.RequireLogin()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
allow, err := prov.AllowGistsWithoutLogin()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !require || (isSingleGistAccess && allow), nil
|
||||
}
|
||||
50
internal/cli/admin.go
Normal file
50
internal/cli/admin.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var CmdAdmin = cli.Command{
|
||||
Name: "admin",
|
||||
Usage: "Admin commands",
|
||||
Subcommands: []*cli.Command{
|
||||
&CmdAdminResetPassword,
|
||||
},
|
||||
}
|
||||
|
||||
var CmdAdminResetPassword = cli.Command{
|
||||
Name: "reset-password",
|
||||
Usage: "Reset the password for a given user",
|
||||
ArgsUsage: "[username] [password]",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
initialize(ctx)
|
||||
if ctx.NArg() < 2 {
|
||||
return fmt.Errorf("username and password are required")
|
||||
}
|
||||
username := ctx.Args().Get(0)
|
||||
plainPassword := ctx.Args().Get(1)
|
||||
|
||||
user, err := db.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
fmt.Printf("Cannot get user %s: %s\n", username, err)
|
||||
return err
|
||||
}
|
||||
password, err := utils.Argon2id.Hash(plainPassword)
|
||||
if err != nil {
|
||||
fmt.Printf("Cannot hash password for user %s: %s\n", username, err)
|
||||
return err
|
||||
}
|
||||
user.Password = password
|
||||
|
||||
if err = user.Update(); err != nil {
|
||||
fmt.Printf("Cannot update password for user %s: %s\n", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Password for user %s has been reset.\n", username)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
56
internal/cli/hook.go
Normal file
56
internal/cli/hook.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/hooks"
|
||||
"github.com/urfave/cli/v2"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var CmdHook = cli.Command{
|
||||
Name: "hook",
|
||||
Usage: "Run Git server hooks, used and should only be called by Opengist itself",
|
||||
Subcommands: []*cli.Command{
|
||||
&CmdHookPreReceive,
|
||||
&CmdHookPostReceive,
|
||||
},
|
||||
}
|
||||
|
||||
var CmdHookPreReceive = cli.Command{
|
||||
Name: "pre-receive",
|
||||
Usage: "Run Git server pre-receive hook for a repository",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
initialize(ctx)
|
||||
if err := hooks.PreReceive(os.Stdin, os.Stdout, os.Stderr); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var CmdHookPostReceive = cli.Command{
|
||||
Name: "post-receive",
|
||||
Usage: "Run Git server post-receive hook for a repository",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
initialize(ctx)
|
||||
if err := hooks.PostReceive(os.Stdin, os.Stdout, os.Stderr); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func initialize(ctx *cli.Context) {
|
||||
if err := config.InitConfig(ctx.String("config"), io.Discard); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
config.InitLog()
|
||||
|
||||
if err := db.Setup(filepath.Join(config.GetHomeDir(), config.C.DBFilename), false); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize database in hooks")
|
||||
}
|
||||
}
|
||||
156
internal/cli/main.go
Normal file
156
internal/cli/main.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"github.com/thomiceli/opengist/internal/memdb"
|
||||
"github.com/thomiceli/opengist/internal/ssh"
|
||||
"github.com/thomiceli/opengist/internal/web"
|
||||
"github.com/urfave/cli/v2"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var CmdVersion = cli.Command{
|
||||
Name: "version",
|
||||
Usage: "Print the version of Opengist",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("Opengist " + config.OpengistVersion)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var CmdStart = cli.Command{
|
||||
Name: "start",
|
||||
Usage: "Start Opengist server",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
Initialize(ctx)
|
||||
go web.NewServer(os.Getenv("OG_DEV") == "1", path.Join(config.GetHomeDir(), "sessions")).Start()
|
||||
go ssh.Start()
|
||||
select {}
|
||||
},
|
||||
}
|
||||
|
||||
var ConfigFlag = cli.StringFlag{
|
||||
Name: "config",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "Path to a config file in YAML format",
|
||||
}
|
||||
|
||||
func App() error {
|
||||
app := cli.NewApp()
|
||||
app.Name = "Opengist"
|
||||
app.Usage = "A self-hosted pastebin powered by Git."
|
||||
app.HelpName = "opengist"
|
||||
|
||||
app.Commands = []*cli.Command{&CmdVersion, &CmdStart, &CmdHook, &CmdAdmin}
|
||||
app.DefaultCommand = CmdStart.Name
|
||||
app.Flags = []cli.Flag{
|
||||
&ConfigFlag,
|
||||
}
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
|
||||
func Initialize(ctx *cli.Context) {
|
||||
fmt.Println("Opengist " + config.OpengistVersion)
|
||||
|
||||
if err := config.InitConfig(ctx.String("config"), os.Stdout); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
config.InitLog()
|
||||
|
||||
gitVersion, err := git.GetGitVersion()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
if ok, err := config.CheckGitVersion(gitVersion); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
} else if !ok {
|
||||
log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.28 and some features would not work. " +
|
||||
"Current git version: " + gitVersion)
|
||||
}
|
||||
|
||||
homePath := config.GetHomeDir()
|
||||
log.Info().Msg("Data directory: " + homePath)
|
||||
|
||||
if err := createSymlink(homePath, ctx.String("config")); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to create symlinks")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "sessions"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "repos"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "custom"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
|
||||
if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), false); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize database")
|
||||
}
|
||||
|
||||
if err := memdb.Setup(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize in memory database")
|
||||
}
|
||||
|
||||
if config.C.IndexEnabled {
|
||||
log.Info().Msg("Index directory: " + filepath.Join(homePath, config.C.IndexDirname))
|
||||
if err := index.Open(filepath.Join(homePath, config.C.IndexDirname)); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open index")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createSymlink(homePath string, configPath string) error {
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "symlinks"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
symlinkExePath := path.Join(config.GetHomeDir(), "symlinks", "opengist")
|
||||
if _, err := os.Lstat(symlinkExePath); err == nil {
|
||||
if err := os.Remove(symlinkExePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = os.Symlink(exePath, symlinkExePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
configPath, _ = filepath.Abs(configPath)
|
||||
configPath = filepath.Clean(configPath)
|
||||
symlinkConfigPath := path.Join(config.GetHomeDir(), "symlinks", "config.yml")
|
||||
if _, err := os.Lstat(symlinkConfigPath); err == nil {
|
||||
if err := os.Remove(symlinkConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = os.Symlink(configPath, symlinkConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,10 +2,12 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -15,7 +17,7 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var OpengistVersion = "1.4.2"
|
||||
var OpengistVersion = ""
|
||||
|
||||
var C *config
|
||||
|
||||
@@ -23,18 +25,20 @@ var C *config
|
||||
// doesn't support dot notation in this case sadly
|
||||
type config struct {
|
||||
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
|
||||
LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"`
|
||||
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
|
||||
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
|
||||
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"`
|
||||
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"`
|
||||
IndexDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"`
|
||||
|
||||
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
|
||||
|
||||
SqliteJournalMode string `yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE"`
|
||||
|
||||
HttpHost string `yaml:"http.host" env:"OG_HTTP_HOST"`
|
||||
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
|
||||
HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"`
|
||||
HttpTLSEnabled bool `yaml:"http.tls-enabled" env:"OG_HTTP_TLS_ENABLED"`
|
||||
HttpCertFile string `yaml:"http.cert-file" env:"OG_HTTP_CERT_FILE"`
|
||||
HttpKeyFile string `yaml:"http.key-file" env:"OG_HTTP_KEY_FILE"`
|
||||
HttpHost string `yaml:"http.host" env:"OG_HTTP_HOST"`
|
||||
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
|
||||
HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"`
|
||||
|
||||
SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"`
|
||||
SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"`
|
||||
@@ -45,60 +49,92 @@ type config struct {
|
||||
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
|
||||
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
|
||||
|
||||
GitlabClientKey string `yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY"`
|
||||
GitlabSecret string `yaml:"gitlab.secret" env:"OG_GITLAB_SECRET"`
|
||||
GitlabUrl string `yaml:"gitlab.url" env:"OG_GITLAB_URL"`
|
||||
GitlabName string `yaml:"gitlab.name" env:"OG_GITLAB_NAME"`
|
||||
|
||||
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
|
||||
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
|
||||
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
|
||||
GiteaName string `yaml:"gitea.name" env:"OG_GITEA_NAME"`
|
||||
|
||||
OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"`
|
||||
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
|
||||
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"`
|
||||
|
||||
CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"`
|
||||
CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"`
|
||||
StaticLinks []StaticLink `yaml:"custom.static-links" env:"OG_CUSTOM_STATIC_LINK"`
|
||||
}
|
||||
|
||||
type StaticLink struct {
|
||||
Name string `yaml:"name" env:"OG_CUSTOM_STATIC_LINK_#_NAME"`
|
||||
Path string `yaml:"path" env:"OG_CUSTOM_STATIC_LINK_#_PATH"`
|
||||
}
|
||||
|
||||
func configWithDefaults() (*config, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
c := &config{}
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
c.LogLevel = "warn"
|
||||
c.OpengistHome = filepath.Join(homeDir, ".opengist")
|
||||
c.LogOutput = "stdout,file"
|
||||
c.OpengistHome = ""
|
||||
c.DBFilename = "opengist.db"
|
||||
c.IndexEnabled = true
|
||||
c.IndexDirname = "opengist.index"
|
||||
|
||||
c.SqliteJournalMode = "WAL"
|
||||
|
||||
c.HttpHost = "0.0.0.0"
|
||||
c.HttpPort = "6157"
|
||||
c.HttpGit = true
|
||||
c.HttpTLSEnabled = false
|
||||
|
||||
c.SshGit = true
|
||||
c.SshHost = "0.0.0.0"
|
||||
c.SshPort = "2222"
|
||||
c.SshKeygen = "ssh-keygen"
|
||||
|
||||
c.GiteaUrl = "http://gitea.com"
|
||||
c.GitlabName = "GitLab"
|
||||
|
||||
c.GiteaUrl = "https://gitea.com"
|
||||
c.GiteaName = "Gitea"
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func InitConfig(configPath string) error {
|
||||
func InitConfig(configPath string, out io.Writer) error {
|
||||
// Default values
|
||||
c, err := configWithDefaults()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = loadConfigFromYaml(c, configPath); err != nil {
|
||||
if err = loadConfigFromYaml(c, configPath, out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = loadConfigFromEnv(c); err != nil {
|
||||
if err = loadConfigFromEnv(c, out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.OpengistHome == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("opengist home directory is not set and current user home directory could not be determined; please specify the opengist home directory manually via the configuration")
|
||||
}
|
||||
|
||||
c.OpengistHome = filepath.Join(homeDir, ".opengist")
|
||||
}
|
||||
|
||||
if err = checks(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
C = c
|
||||
|
||||
if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -106,21 +142,46 @@ func InitLog() {
|
||||
if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var level zerolog.Level
|
||||
level, err = zerolog.ParseLevel(C.LogLevel)
|
||||
level, err := zerolog.ParseLevel(C.LogLevel)
|
||||
if err != nil {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
|
||||
multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file)
|
||||
var logWriters []io.Writer
|
||||
logOutputTypes := utils.RemoveDuplicates[string](
|
||||
strings.Split(strings.ToLower(C.LogOutput), ","),
|
||||
)
|
||||
for _, logOutputType := range logOutputTypes {
|
||||
logOutputType = strings.TrimSpace(logOutputType)
|
||||
if !slices.Contains([]string{"stdout", "file"}, logOutputType) {
|
||||
defer func() { log.Warn().Msg("Invalid log output type: " + logOutputType) }()
|
||||
continue
|
||||
}
|
||||
|
||||
switch logOutputType {
|
||||
case "stdout":
|
||||
logWriters = append(logWriters, zerolog.NewConsoleWriter())
|
||||
defer func() { log.Debug().Msg("Logging to stdout") }()
|
||||
case "file":
|
||||
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logWriters = append(logWriters, file)
|
||||
defer func() { log.Debug().Msg("Logging to file: " + file.Name()) }()
|
||||
}
|
||||
}
|
||||
if len(logWriters) == 0 {
|
||||
logWriters = append(logWriters, zerolog.NewConsoleWriter())
|
||||
defer func() { log.Warn().Msg("No valid log outputs, defaulting to stdout") }()
|
||||
}
|
||||
|
||||
multi := zerolog.MultiLevelWriter(logWriters...)
|
||||
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
|
||||
|
||||
if !utils.SliceContains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
|
||||
if !slices.Contains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
|
||||
log.Warn().Msg("Invalid log level: " + C.LogLevel)
|
||||
}
|
||||
}
|
||||
@@ -139,8 +200,8 @@ func CheckGitVersion(version string) (bool, error) {
|
||||
return false, fmt.Errorf("invalid minor version number")
|
||||
}
|
||||
|
||||
// Check if version is prior to 2.20
|
||||
if major < 2 || (major == 2 && minor < 20) {
|
||||
// Check if version is prior to 2.28
|
||||
if major < 2 || (major == 2 && minor < 28) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
@@ -151,7 +212,7 @@ func GetHomeDir() string {
|
||||
return filepath.Clean(absolutePath)
|
||||
}
|
||||
|
||||
func loadConfigFromYaml(c *config, configPath string) error {
|
||||
func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
|
||||
if configPath != "" {
|
||||
absolutePath, _ := filepath.Abs(configPath)
|
||||
absolutePath = filepath.Clean(absolutePath)
|
||||
@@ -160,9 +221,9 @@ func loadConfigFromYaml(c *config, configPath string) error {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
fmt.Println("No YAML config file found at " + absolutePath)
|
||||
_, _ = fmt.Fprintln(out, "No YAML config file found at "+absolutePath)
|
||||
} else {
|
||||
fmt.Println("Using YAML config file: " + absolutePath)
|
||||
_, _ = fmt.Fprintln(out, "Using YAML config file: "+absolutePath)
|
||||
|
||||
// Override default values with values from config.yml
|
||||
d := yaml.NewDecoder(file)
|
||||
@@ -172,24 +233,13 @@ func loadConfigFromYaml(c *config, configPath string) error {
|
||||
defer file.Close()
|
||||
}
|
||||
} else {
|
||||
fmt.Println("No YAML config file specified.")
|
||||
}
|
||||
|
||||
// Override default values with environment variables (as yaml)
|
||||
configEnv := os.Getenv("CONFIG")
|
||||
if configEnv != "" {
|
||||
fmt.Println("Using config from environment variable: CONFIG")
|
||||
fmt.Println("!! This method of setting the config is deprecated and will be removed in a future version of Opengist")
|
||||
d := yaml.NewDecoder(strings.NewReader(configEnv))
|
||||
if err := d.Decode(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(out, "No YAML config file specified.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadConfigFromEnv(c *config) error {
|
||||
func loadConfigFromEnv(c *config, out io.Writer) error {
|
||||
v := reflect.ValueOf(c).Elem()
|
||||
var envVars []string
|
||||
|
||||
@@ -201,28 +251,69 @@ func loadConfigFromEnv(c *config) error {
|
||||
}
|
||||
|
||||
envValue := os.Getenv(strings.ToUpper(tag))
|
||||
if envValue == "" {
|
||||
if envValue == "" && v.Field(i).Kind() != reflect.Slice {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v.Field(i).Kind() {
|
||||
case reflect.String:
|
||||
v.Field(i).SetString(envValue)
|
||||
envVars = append(envVars, tag)
|
||||
case reflect.Bool:
|
||||
boolVal, err := strconv.ParseBool(envValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Field(i).SetBool(boolVal)
|
||||
envVars = append(envVars, tag)
|
||||
case reflect.Slice:
|
||||
if v.Type().Field(i).Type.Elem().Kind() == reflect.Struct {
|
||||
prefix := strings.ToUpper(tag) + "_"
|
||||
var sliceValue reflect.Value
|
||||
elemType := v.Type().Field(i).Type.Elem()
|
||||
|
||||
for index := 0; ; index++ {
|
||||
allFieldsPresent := true
|
||||
elemValue := reflect.New(elemType).Elem()
|
||||
|
||||
for j := 0; j < elemValue.NumField() && allFieldsPresent; j++ {
|
||||
elemField := elemValue.Type().Field(j)
|
||||
envName := fmt.Sprintf("%s%d_%s", prefix, index, strings.ToUpper(elemField.Name))
|
||||
envValue, present := os.LookupEnv(envName)
|
||||
|
||||
if !present {
|
||||
allFieldsPresent = false
|
||||
break
|
||||
}
|
||||
|
||||
envVars = append(envVars, envName)
|
||||
elemValue.Field(j).SetString(envValue)
|
||||
}
|
||||
|
||||
if !allFieldsPresent {
|
||||
break
|
||||
}
|
||||
|
||||
if sliceValue.Kind() != reflect.Slice {
|
||||
sliceValue = reflect.MakeSlice(v.Type().Field(i).Type, 0, index+1)
|
||||
}
|
||||
sliceValue = reflect.Append(sliceValue, elemValue)
|
||||
}
|
||||
|
||||
if sliceValue.IsValid() {
|
||||
v.Field(i).Set(sliceValue)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %s", v.Field(i).Kind())
|
||||
}
|
||||
|
||||
envVars = append(envVars, tag)
|
||||
}
|
||||
|
||||
if len(envVars) > 0 {
|
||||
fmt.Println("Using environment variables config: " + strings.Join(envVars, ", "))
|
||||
_, _ = fmt.Fprintln(out, "Using environment variables config: "+strings.Join(envVars, ", "))
|
||||
} else {
|
||||
fmt.Println("No environment variables config specified.")
|
||||
_, _ = fmt.Fprintln(out, "No environment variables config specified.")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -237,5 +328,9 @@ func checks(c *config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := url.Parse(c.OIDCDiscoveryUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -10,10 +10,11 @@ type AdminSetting struct {
|
||||
}
|
||||
|
||||
const (
|
||||
SettingDisableSignup = "disable-signup"
|
||||
SettingRequireLogin = "require-login"
|
||||
SettingDisableLoginForm = "disable-login-form"
|
||||
SettingDisableGravatar = "disable-gravatar"
|
||||
SettingDisableSignup = "disable-signup"
|
||||
SettingRequireLogin = "require-login"
|
||||
SettingAllowGistsWithoutLogin = "allow-gists-without-login"
|
||||
SettingDisableLoginForm = "disable-login-form"
|
||||
SettingDisableGravatar = "disable-gravatar"
|
||||
)
|
||||
|
||||
func GetSetting(key string) (string, error) {
|
||||
@@ -62,3 +63,21 @@ func initAdminSettings(settings map[string]string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DBAuthInfo struct{}
|
||||
|
||||
func (auth DBAuthInfo) RequireLogin() (bool, error) {
|
||||
s, err := GetSetting(SettingRequireLogin)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return s == "1", nil
|
||||
}
|
||||
|
||||
func (auth DBAuthInfo) AllowGistsWithoutLogin() (bool, error) {
|
||||
s, err := GetSetting(SettingAllowGistsWithoutLogin)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s == "1", nil
|
||||
}
|
||||
@@ -1,28 +1,34 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
msqlite "github.com/glebarez/go-sqlite"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func Setup(dbPath string) error {
|
||||
func Setup(dbPath string, sharedCache bool) error {
|
||||
var err error
|
||||
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
|
||||
|
||||
if !utils.SliceContains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
|
||||
if !slices.Contains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
|
||||
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
|
||||
}
|
||||
|
||||
if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode), &gorm.Config{
|
||||
sharedCacheStr := ""
|
||||
if sharedCache {
|
||||
sharedCacheStr = "&cache=shared"
|
||||
}
|
||||
|
||||
if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -36,21 +42,32 @@ func Setup(dbPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil {
|
||||
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ApplyMigrations(db)
|
||||
if err = ApplyMigrations(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Default admin setting values
|
||||
return initAdminSettings(map[string]string{
|
||||
SettingDisableSignup: "0",
|
||||
SettingRequireLogin: "0",
|
||||
SettingDisableLoginForm: "0",
|
||||
SettingDisableGravatar: "0",
|
||||
SettingDisableSignup: "0",
|
||||
SettingRequireLogin: "0",
|
||||
SettingAllowGistsWithoutLogin: "0",
|
||||
SettingDisableLoginForm: "0",
|
||||
SettingDisableGravatar: "0",
|
||||
})
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
|
||||
func CountAll(table interface{}) (int64, error) {
|
||||
var count int64
|
||||
err := db.Model(table).Count(&count).Error
|
||||
@@ -58,9 +75,18 @@ func CountAll(table interface{}) (int64, error) {
|
||||
}
|
||||
|
||||
func IsUniqueConstraintViolation(err error) bool {
|
||||
var sqliteErr sqlite3.Error
|
||||
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
var sqliteErr *msqlite.Error
|
||||
if errors.As(err, &sqliteErr) && sqliteErr.Code() == 2067 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Ping() error {
|
||||
sql, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sql.Ping()
|
||||
}
|
||||
@@ -1,21 +1,75 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"gorm.io/gorm"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Visibility int
|
||||
|
||||
const (
|
||||
PublicVisibility Visibility = iota
|
||||
UnlistedVisibility
|
||||
PrivateVisibility
|
||||
)
|
||||
|
||||
func (v Visibility) String() string {
|
||||
switch v {
|
||||
case PublicVisibility:
|
||||
return "public"
|
||||
case UnlistedVisibility:
|
||||
return "unlisted"
|
||||
case PrivateVisibility:
|
||||
return "private"
|
||||
default:
|
||||
return "???"
|
||||
}
|
||||
}
|
||||
|
||||
func (v Visibility) Next() Visibility {
|
||||
switch v {
|
||||
case PublicVisibility:
|
||||
return UnlistedVisibility
|
||||
case UnlistedVisibility:
|
||||
return PrivateVisibility
|
||||
default:
|
||||
return PublicVisibility
|
||||
}
|
||||
}
|
||||
|
||||
func ParseVisibility[T string | int](v T) (Visibility, error) {
|
||||
switch s := fmt.Sprint(v); s {
|
||||
case "0", "public":
|
||||
return PublicVisibility, nil
|
||||
case "1", "unlisted":
|
||||
return UnlistedVisibility, nil
|
||||
case "2", "private":
|
||||
return PrivateVisibility, nil
|
||||
default:
|
||||
return -1, fmt.Errorf("unknown visibility %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
type Gist struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Uuid string
|
||||
Title string
|
||||
URL string
|
||||
Preview string
|
||||
PreviewFilename string
|
||||
Description string
|
||||
Private bool
|
||||
Private Visibility // 0: public, 1: unlisted, 2: private
|
||||
UserID uint
|
||||
User User
|
||||
NbFiles int
|
||||
@@ -47,7 +101,7 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
||||
func GetGist(user string, gistUuid string) (*Gist, error) {
|
||||
gist := new(Gist)
|
||||
err := db.Preload("User").Preload("Forked.User").
|
||||
Where("gists.uuid = ? AND users.username like ?", gistUuid, user).
|
||||
Where("(gists.uuid = ? OR gists.url = ?) AND users.username like ?", gistUuid, gistUuid, user).
|
||||
Joins("join users on gists.user_id = users.id").
|
||||
First(&gist).Error
|
||||
|
||||
@@ -89,7 +143,7 @@ func GetAllGists(offset int) ([]*Gist, error) {
|
||||
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Preload("User").Preload("Forked.User").
|
||||
Where("((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId).
|
||||
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
|
||||
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%").
|
||||
Limit(11).
|
||||
Offset(offset * 10).
|
||||
@@ -101,7 +155,7 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st
|
||||
|
||||
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
return db.Preload("User").Preload("Forked.User").
|
||||
Where("((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId).
|
||||
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
|
||||
Where("users.id = ?", fromUserId).
|
||||
Joins("join users on gists.user_id = users.id")
|
||||
}
|
||||
@@ -124,7 +178,7 @@ func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
|
||||
|
||||
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
return db.Preload("User").Preload("Forked.User").
|
||||
Where("((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId).
|
||||
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
|
||||
Where("likes.user_id = ?", fromUserId).
|
||||
Joins("join likes on gists.id = likes.gist_id").
|
||||
Joins("join users on likes.user_id = users.id")
|
||||
@@ -147,7 +201,7 @@ func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error
|
||||
|
||||
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||
return db.Preload("User").Preload("Forked.User").
|
||||
Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId).
|
||||
Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
|
||||
Where("gists.user_id = ?", fromUserId).
|
||||
Joins("join users on gists.user_id = users.id")
|
||||
}
|
||||
@@ -176,6 +230,25 @@ func GetAllGistsRows() ([]*Gist, error) {
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func GetAllGistsVisibleByUser(userId uint) ([]uint, error) {
|
||||
var gists []uint
|
||||
|
||||
err := db.Table("gists").
|
||||
Where("gists.private = 0 or gists.user_id = ?", userId).
|
||||
Pluck("gists.id", &gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func GetAllGistsByIds(ids []uint) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Preload("User").Preload("Forked.User").
|
||||
Where("id in ?", ids).
|
||||
Find(&gists).Error
|
||||
|
||||
return gists, err
|
||||
}
|
||||
|
||||
func (gist *Gist) Create() error {
|
||||
// avoids foreign key constraint error because the default value in the struct is 0
|
||||
return db.Omit("forked_id").Create(&gist).Error
|
||||
@@ -189,7 +262,16 @@ func (gist *Gist) Update() error {
|
||||
return db.Omit("forked_id").Save(&gist).Error
|
||||
}
|
||||
|
||||
func (gist *Gist) UpdateNoTimestamps() error {
|
||||
return db.Omit("forked_id", "updated_at").Save(&gist).Error
|
||||
}
|
||||
|
||||
func (gist *Gist) Delete() error {
|
||||
err := gist.DeleteRepository()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Delete(&gist).Error
|
||||
}
|
||||
|
||||
@@ -243,7 +325,7 @@ func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) {
|
||||
var gists []*Gist
|
||||
err := db.Model(&gist).Preload("User").
|
||||
Where("forked_id = ?", gist.ID).
|
||||
Where("(gists.private = 0) or (gists.private = 1 and gists.user_id = ?)", currentUserId).
|
||||
Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId).
|
||||
Limit(11).
|
||||
Offset(offset * 10).
|
||||
Order("updated_at desc").
|
||||
@@ -264,25 +346,25 @@ func (gist *Gist) DeleteRepository() error {
|
||||
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) Files(revision string) ([]*git.File, error) {
|
||||
var files []*git.File
|
||||
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
|
||||
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
|
||||
filesCat, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
|
||||
if err != nil {
|
||||
// if the revision or the file do not exist
|
||||
|
||||
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
|
||||
return nil, nil
|
||||
return nil, &git.RevisionNotFoundError{}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, fileStr := range filesStr {
|
||||
file, err := gist.File(revision, fileStr, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, file)
|
||||
var files []*git.File
|
||||
for _, fileCat := range filesCat {
|
||||
files = append(files, &git.File{
|
||||
Filename: fileCat.Name,
|
||||
Size: fileCat.Size,
|
||||
HumanSize: humanize.IBytes(fileCat.Size),
|
||||
Content: fileCat.Content,
|
||||
Truncated: fileCat.Truncated,
|
||||
})
|
||||
}
|
||||
return files, err
|
||||
}
|
||||
@@ -295,23 +377,36 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var size uint64
|
||||
|
||||
size, err = git.GetFileSize(gist.User.Username, gist.Uuid, revision, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &git.File{
|
||||
Filename: filename,
|
||||
Size: size,
|
||||
HumanSize: humanize.IBytes(size),
|
||||
Content: content,
|
||||
Truncated: truncated,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (gist *Gist) FileNames(revision string) ([]string, error) {
|
||||
return git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
|
||||
}
|
||||
|
||||
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
|
||||
return git.GetLog(gist.User.Username, gist.Uuid, skip)
|
||||
}
|
||||
|
||||
func (gist *Gist) NbCommits() (string, error) {
|
||||
return git.GetNumberOfCommitsOfRepository(gist.User.Username, gist.Uuid)
|
||||
return git.CountCommits(gist.User.Username, gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
|
||||
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email); err != nil {
|
||||
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -332,6 +427,26 @@ func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
|
||||
return git.Push(gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) AddAndCommitFile(file *FileDTO) error {
|
||||
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := git.AddAll(gist.Uuid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := git.CommitRepository(gist.Uuid, gist.User.Username, gist.User.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return git.Push(gist.Uuid)
|
||||
}
|
||||
|
||||
func (gist *Gist) ForkClone(username string, uuid string) error {
|
||||
return git.ForkClone(gist.User.Username, gist.Uuid, username, uuid)
|
||||
}
|
||||
@@ -344,7 +459,7 @@ func (gist *Gist) RPC(service string) ([]byte, error) {
|
||||
return git.RPC(gist.User.Username, gist.Uuid, service)
|
||||
}
|
||||
|
||||
func (gist *Gist) UpdatePreviewAndCount() error {
|
||||
func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
|
||||
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -367,24 +482,77 @@ func (gist *Gist) UpdatePreviewAndCount() error {
|
||||
gist.Preview = file.Content
|
||||
}
|
||||
|
||||
gist.Preview = file.Content
|
||||
gist.PreviewFilename = file.Filename
|
||||
}
|
||||
|
||||
return gist.Update()
|
||||
if withTimestampUpdate {
|
||||
return gist.Update()
|
||||
}
|
||||
return gist.UpdateNoTimestamps()
|
||||
}
|
||||
|
||||
func (gist *Gist) VisibilityStr() string {
|
||||
switch gist.Private {
|
||||
case PublicVisibility:
|
||||
return "public"
|
||||
case UnlistedVisibility:
|
||||
return "unlisted"
|
||||
case PrivateVisibility:
|
||||
return "private"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (gist *Gist) Identifier() string {
|
||||
if gist.URL != "" {
|
||||
return gist.URL
|
||||
}
|
||||
return gist.Uuid
|
||||
}
|
||||
|
||||
func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
|
||||
files, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
languages := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
var lexer chroma.Lexer
|
||||
if lexer = lexers.Get(file.Filename); lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
fileType := lexer.Config().Name
|
||||
if lexer.Config().Name == "fallback" || lexer.Config().Name == "plaintext" {
|
||||
fileType = "Text"
|
||||
}
|
||||
|
||||
languages = append(languages, fileType)
|
||||
}
|
||||
|
||||
return languages, nil
|
||||
}
|
||||
|
||||
// -- DTO -- //
|
||||
|
||||
type GistDTO struct {
|
||||
Title string `validate:"max=50" form:"title"`
|
||||
Description string `validate:"max=150" form:"description"`
|
||||
Private bool `form:"private"`
|
||||
Title string `validate:"max=250" form:"title"`
|
||||
Description string `validate:"max=1000" form:"description"`
|
||||
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
|
||||
Files []FileDTO `validate:"min=1,dive"`
|
||||
Name []string `form:"name"`
|
||||
Content []string `form:"content"`
|
||||
VisibilityDTO
|
||||
}
|
||||
|
||||
type VisibilityDTO struct {
|
||||
Private Visibility `validate:"number,min=0,max=2" form:"private"`
|
||||
}
|
||||
|
||||
type FileDTO struct {
|
||||
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=50"`
|
||||
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=255"`
|
||||
Content string `validate:"required"`
|
||||
}
|
||||
|
||||
@@ -393,12 +561,84 @@ func (dto *GistDTO) ToGist() *Gist {
|
||||
Title: dto.Title,
|
||||
Description: dto.Description,
|
||||
Private: dto.Private,
|
||||
URL: dto.URL,
|
||||
}
|
||||
}
|
||||
|
||||
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
|
||||
gist.Title = dto.Title
|
||||
gist.Description = dto.Description
|
||||
gist.Private = dto.Private
|
||||
gist.URL = dto.URL
|
||||
return gist
|
||||
}
|
||||
|
||||
// -- Index -- //
|
||||
|
||||
func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
|
||||
files, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exts := make([]string, 0, len(files))
|
||||
wholeContent := ""
|
||||
for _, file := range files {
|
||||
wholeContent += file.Content
|
||||
exts = append(exts, filepath.Ext(file.Filename))
|
||||
}
|
||||
|
||||
fileNames, err := gist.FileNames("HEAD")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
langs, err := gist.GetLanguagesFromFiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexedGist := &index.Gist{
|
||||
GistID: gist.ID,
|
||||
Username: gist.User.Username,
|
||||
Title: gist.Title,
|
||||
Content: wholeContent,
|
||||
Filenames: fileNames,
|
||||
Extensions: exts,
|
||||
Languages: langs,
|
||||
CreatedAt: gist.CreatedAt,
|
||||
UpdatedAt: gist.UpdatedAt,
|
||||
}
|
||||
|
||||
return indexedGist, nil
|
||||
}
|
||||
|
||||
func (gist *Gist) AddInIndex() {
|
||||
if !index.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
indexedGist, err := gist.ToIndexedGist()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
|
||||
return
|
||||
}
|
||||
err = index.AddInIndex(indexedGist)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error adding gist %d to index", gist.ID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (gist *Gist) RemoveFromIndex() {
|
||||
if !index.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := index.RemoveFromIndex(gist.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Error remove gist %d from index", gist.ID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
87
internal/db/invitation.go
Normal file
87
internal/db/invitation.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Invitation struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Code string
|
||||
ExpiresAt int64
|
||||
NbUsed uint
|
||||
NbMax uint
|
||||
}
|
||||
|
||||
func GetAllInvitations() ([]*Invitation, error) {
|
||||
var invitations []*Invitation
|
||||
err := db.
|
||||
Order("(((expires_at >= strftime('%s', 'now')) AND ((nb_max <= 0) OR (nb_used < nb_max)))) desc").
|
||||
Order("id asc").
|
||||
Find(&invitations).Error
|
||||
|
||||
return invitations, err
|
||||
}
|
||||
|
||||
func GetInvitationByID(id uint) (*Invitation, error) {
|
||||
invitation := new(Invitation)
|
||||
err := db.
|
||||
Where("id = ?", id).
|
||||
First(&invitation).Error
|
||||
return invitation, err
|
||||
}
|
||||
|
||||
func GetInvitationByCode(code string) (*Invitation, error) {
|
||||
invitation := new(Invitation)
|
||||
err := db.
|
||||
Where("code = ?", code).
|
||||
First(&invitation).Error
|
||||
return invitation, err
|
||||
}
|
||||
|
||||
func InvitationCodeExists(code string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Model(&Invitation{}).Where("code = ?", code).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (i *Invitation) Create() error {
|
||||
i.Code = generateRandomCode()
|
||||
return db.Create(&i).Error
|
||||
}
|
||||
|
||||
func (i *Invitation) Update() error {
|
||||
return db.Save(&i).Error
|
||||
}
|
||||
|
||||
func (i *Invitation) Delete() error {
|
||||
return db.Delete(&i).Error
|
||||
}
|
||||
|
||||
func (i *Invitation) IsExpired() bool {
|
||||
return i.ExpiresAt < time.Now().Unix()
|
||||
}
|
||||
|
||||
func (i *Invitation) IsMaxedOut() bool {
|
||||
return i.NbMax > 0 && i.NbUsed >= i.NbMax
|
||||
}
|
||||
|
||||
func (i *Invitation) IsUsable() bool {
|
||||
return !i.IsExpired() && !i.IsMaxedOut()
|
||||
}
|
||||
|
||||
func (i *Invitation) Use() error {
|
||||
i.NbUsed++
|
||||
return i.Update()
|
||||
}
|
||||
|
||||
func generateRandomCode() string {
|
||||
const charset = "0123456789ABCDEF"
|
||||
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
result := make([]byte, 16)
|
||||
|
||||
for i := range result {
|
||||
result[i] = charset[seededRand.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@@ -19,7 +19,7 @@ type SSHKey struct {
|
||||
User User `validate:"-" `
|
||||
}
|
||||
|
||||
func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error {
|
||||
func (sshKey *SSHKey) BeforeCreate(*gorm.DB) error {
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -48,13 +48,12 @@ func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
|
||||
return sshKey, err
|
||||
}
|
||||
|
||||
func SSHKeyDoesExists(sshKeyContent string) (*SSHKey, error) {
|
||||
sshKey := new(SSHKey)
|
||||
err := db.
|
||||
Where("content like ?", sshKeyContent+"%").
|
||||
First(&sshKey).Error
|
||||
|
||||
return sshKey, err
|
||||
func SSHKeyDoesExists(sshKeyContent string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Model(&SSHKey{}).
|
||||
Where("content = ?", sshKeyContent).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (sshKey *SSHKey) Create() error {
|
||||
@@ -1,4 +1,4 @@
|
||||
package models
|
||||
package db
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
@@ -14,7 +14,9 @@ type User struct {
|
||||
MD5Hash string // for gravatar, if no Email is specified, the value is random
|
||||
AvatarURL string
|
||||
GithubID string
|
||||
GitlabID string
|
||||
GiteaID string
|
||||
OIDCID string `gorm:"column:oidc_id"`
|
||||
|
||||
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
@@ -38,7 +40,7 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
|
||||
}
|
||||
|
||||
// Decrement forks counter for all gists forked by this user
|
||||
return tx.Model(&Gist{}).
|
||||
err = tx.Model(&Gist{}).
|
||||
Omit("updated_at").
|
||||
Where("id IN (?)", tx.
|
||||
Select("forked_id").
|
||||
@@ -47,6 +49,17 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
|
||||
).
|
||||
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")).
|
||||
Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Where("user_id = ?", user.ID).Delete(&SSHKey{}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete all gists created by this user
|
||||
return tx.Where("user_id = ?", user.ID).Delete(&Gist{}).Error
|
||||
}
|
||||
|
||||
func UserExists(username string) (bool, error) {
|
||||
@@ -93,7 +106,6 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
|
||||
err := db.
|
||||
Where("email IN ?", emails).
|
||||
Find(&users).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -106,6 +118,15 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
|
||||
return userMap, nil
|
||||
}
|
||||
|
||||
func GetUserFromSSHKey(sshKey string) (*User, error) {
|
||||
user := new(User)
|
||||
err := db.
|
||||
Joins("JOIN ssh_keys ON users.id = ssh_keys.user_id").
|
||||
Where("ssh_keys.content = ?", sshKey).
|
||||
First(&user).Error
|
||||
return user, err
|
||||
}
|
||||
|
||||
func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
|
||||
key := new(SSHKey)
|
||||
err := db.
|
||||
@@ -122,8 +143,12 @@ func GetUserByProvider(id string, provider string) (*User, error) {
|
||||
switch provider {
|
||||
case "github":
|
||||
err = db.Where("github_id = ?", id).First(&user).Error
|
||||
case "gitlab":
|
||||
err = db.Where("gitlab_id = ?", id).First(&user).Error
|
||||
case "gitea":
|
||||
err = db.Where("gitea_id = ?", id).First(&user).Error
|
||||
case "openid-connect":
|
||||
err = db.Where("oidc_id = ?", id).First(&user).Error
|
||||
}
|
||||
|
||||
return user, err
|
||||
@@ -158,15 +183,16 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
|
||||
}
|
||||
|
||||
func (user *User) DeleteProviderID(provider string) error {
|
||||
switch provider {
|
||||
case "github":
|
||||
providerIDFields := map[string]string{
|
||||
"github": "github_id",
|
||||
"gitlab": "gitlab_id",
|
||||
"gitea": "gitea_id",
|
||||
"openid-connect": "oidc_id",
|
||||
}
|
||||
|
||||
if providerIDField, ok := providerIDFields[provider]; ok {
|
||||
return db.Model(&user).
|
||||
Update("github_id", nil).
|
||||
Update("avatar_url", nil).
|
||||
Error
|
||||
case "gitea":
|
||||
return db.Model(&user).
|
||||
Update("gitea_id", nil).
|
||||
Update(providerIDField, nil).
|
||||
Update("avatar_url", nil).
|
||||
Error
|
||||
}
|
||||
@@ -177,7 +203,7 @@ func (user *User) DeleteProviderID(provider string) error {
|
||||
// -- DTO -- //
|
||||
|
||||
type UserDTO struct {
|
||||
Username string `form:"username" validate:"required,max=24,alphanum,notreserved"`
|
||||
Username string `form:"username" validate:"required,max=24,alphanumdash,notreserved"`
|
||||
Password string `form:"password" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,18 +1,57 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
ReposDirectory = "repos"
|
||||
)
|
||||
|
||||
const truncateLimit = 2 << 18
|
||||
const diffSize = 2 << 12
|
||||
const maxFilesPerDiffCommit = 10
|
||||
|
||||
type RevisionNotFoundError struct{}
|
||||
|
||||
func (m *RevisionNotFoundError) Error() string {
|
||||
return "revision not found"
|
||||
}
|
||||
|
||||
func RepositoryPath(user string, gist string) string {
|
||||
return filepath.Join(config.GetHomeDir(), "repos", strings.ToLower(user), gist)
|
||||
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist)
|
||||
}
|
||||
|
||||
func RepositoryUrl(ctx echo.Context, user string, gist string) string {
|
||||
httpProtocol := "http"
|
||||
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
|
||||
httpProtocol = "https"
|
||||
}
|
||||
|
||||
var baseHttpUrl string
|
||||
// if a custom external url is set, use it
|
||||
if config.C.ExternalUrl != "" {
|
||||
baseHttpUrl = config.C.ExternalUrl
|
||||
} else {
|
||||
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s/%s", baseHttpUrl, user, gist)
|
||||
}
|
||||
|
||||
func TmpRepositoryPath(gistId string) string {
|
||||
@@ -27,22 +66,23 @@ func TmpRepositoriesPath() string {
|
||||
func InitRepository(user string, gist string) error {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
cmd := exec.Command(
|
||||
"git",
|
||||
"init",
|
||||
"--bare",
|
||||
repositoryPath,
|
||||
)
|
||||
var args []string
|
||||
args = append(args, "init")
|
||||
if config.C.GitDefaultBranch != "" {
|
||||
args = append(args, "--initial-branch", config.C.GitDefaultBranch)
|
||||
}
|
||||
args = append(args, "--bare", repositoryPath)
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
cmd := exec.Command("git", args...)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return copyFiles(repositoryPath)
|
||||
return CreateDotGitFiles(user, gist)
|
||||
}
|
||||
|
||||
func GetNumberOfCommitsOfRepository(user string, gist string) (string, error) {
|
||||
func CountCommits(user string, gist string) (string, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
cmd := exec.Command(
|
||||
@@ -78,15 +118,134 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
|
||||
return slice[:len(slice)-1], nil
|
||||
}
|
||||
|
||||
type catFileBatch struct {
|
||||
Name, Hash, Content string
|
||||
Size uint64
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*catFileBatch, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
lsTreeCmd := exec.Command("git", "ls-tree", "-l", revision)
|
||||
lsTreeCmd.Dir = repositoryPath
|
||||
lsTreeOutput, err := lsTreeCmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileMap := make([]*catFileBatch, 0)
|
||||
|
||||
lines := strings.Split(string(lsTreeOutput), "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 4 {
|
||||
continue // Skip lines that don't have enough fields
|
||||
}
|
||||
|
||||
hash := fields[2]
|
||||
size, err := strconv.ParseUint(fields[3], 10, 64)
|
||||
if err != nil {
|
||||
continue // Skip lines with invalid size field
|
||||
}
|
||||
name := strings.Join(fields[4:], " ") // File name may contain spaces
|
||||
|
||||
fileMap = append(fileMap, &catFileBatch{
|
||||
Hash: hash,
|
||||
Size: size,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
catFileCmd := exec.Command("git", "cat-file", "--batch")
|
||||
catFileCmd.Dir = repositoryPath
|
||||
stdin, err := catFileCmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdout, err := catFileCmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = catFileCmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(stdout)
|
||||
|
||||
for _, file := range fileMap {
|
||||
_, err = stdin.Write([]byte(file.Hash + "\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parts := strings.Fields(header)
|
||||
if len(parts) > 3 {
|
||||
continue // Not a valid header, skip this entry
|
||||
}
|
||||
|
||||
size, err := strconv.ParseUint(parts[2], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sizeToRead := size
|
||||
if truncate && sizeToRead > truncateLimit {
|
||||
sizeToRead = truncateLimit
|
||||
}
|
||||
|
||||
// Read exactly size bytes from header, or the max allowed if truncated
|
||||
content := make([]byte, sizeToRead)
|
||||
if _, err = io.ReadFull(reader, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file.Content = string(content)
|
||||
|
||||
if truncate && size > truncateLimit {
|
||||
// skip other bytes if truncated
|
||||
if _, err = reader.Discard(int(size - truncateLimit)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file.Truncated = true
|
||||
}
|
||||
|
||||
// Read the blank line following the content
|
||||
if _, err := reader.ReadByte(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = stdin.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = catFileCmd.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fileMap, nil
|
||||
}
|
||||
|
||||
func GetFileContent(user string, gist string, revision string, filename string, truncate bool) (string, bool, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
var maxBytes int64 = -1
|
||||
if truncate {
|
||||
maxBytes = 2 << 18
|
||||
maxBytes = truncateLimit
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
// Set up a context with a timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
"git",
|
||||
"--no-pager",
|
||||
"show",
|
||||
@@ -94,14 +253,36 @@ func GetFileContent(user string, gist string, revision string, filename string,
|
||||
)
|
||||
cmd.Dir = repositoryPath
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
err := cmd.Start()
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
defer cmd.Wait()
|
||||
|
||||
return truncateCommandOutput(stdout, maxBytes)
|
||||
content, truncated, err := truncateCommandOutput(bytes.NewReader(output), maxBytes)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return content, truncated, nil
|
||||
}
|
||||
|
||||
func GetFileSize(user string, gist string, revision string, filename string) (uint64, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
cmd := exec.Command(
|
||||
"git",
|
||||
"cat-file",
|
||||
"-s",
|
||||
revision+":"+filename,
|
||||
)
|
||||
cmd.Dir = repositoryPath
|
||||
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.ParseUint(strings.TrimSuffix(string(stdout), "\n"), 10, 64)
|
||||
}
|
||||
|
||||
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
||||
@@ -127,12 +308,17 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cmd.Wait()
|
||||
defer func(cmd *exec.Cmd) {
|
||||
waitErr := cmd.Wait()
|
||||
if waitErr != nil {
|
||||
err = waitErr
|
||||
}
|
||||
}(cmd)
|
||||
|
||||
return parseLog(stdout, 2<<18), nil
|
||||
return parseLog(stdout, maxFilesPerDiffCommit, diffSize)
|
||||
}
|
||||
|
||||
func CloneTmp(user string, gist string, gistTmpId string, email string) error {
|
||||
func CloneTmp(user string, gist string, gistTmpId string, email string, remove bool) error {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
tmpPath := TmpRepositoriesPath()
|
||||
@@ -150,13 +336,13 @@ func CloneTmp(user string, gist string, gistTmpId string, email string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove every file (and not the .git directory!)
|
||||
cmd = exec.Command("find", ".", "-maxdepth", "1", "-type", "f", "-delete")
|
||||
cmd.Dir = tmpRepositoryPath
|
||||
if err = cmd.Run(); err != nil {
|
||||
return err
|
||||
// remove every file (keep the .git directory)
|
||||
// useful when user wants to edit multiple files from an existing gist
|
||||
if remove {
|
||||
if err = removeFilesExceptGit(tmpRepositoryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmd = exec.Command("git", "config", "--local", "user.name", user)
|
||||
cmd.Dir = tmpRepositoryPath
|
||||
if err = cmd.Run(); err != nil {
|
||||
@@ -177,7 +363,7 @@ func ForkClone(userSrc string, gistSrc string, userDst string, gistDst string) e
|
||||
return err
|
||||
}
|
||||
|
||||
return copyFiles(repositoryPathDst)
|
||||
return CreateDotGitFiles(userDst, gistDst)
|
||||
}
|
||||
|
||||
func SetFileContent(gistTmpId string, filename string, content string) error {
|
||||
@@ -230,7 +416,6 @@ func Push(gistTmpId string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.RemoveAll(tmpRepositoryPath)
|
||||
}
|
||||
|
||||
@@ -255,6 +440,67 @@ func RPC(user string, gist string, service string) ([]byte, error) {
|
||||
return stdout, err
|
||||
}
|
||||
|
||||
func GcRepos() error {
|
||||
subdirs, err := os.ReadDir(filepath.Join(config.GetHomeDir(), ReposDirectory))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, subdir := range subdirs {
|
||||
if !subdir.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
subRoot := filepath.Join(config.GetHomeDir(), ReposDirectory, subdir.Name())
|
||||
|
||||
gitRepos, err := os.ReadDir(subRoot)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Cannot read directory")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, repo := range gitRepos {
|
||||
if !repo.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
repoPath := filepath.Join(subRoot, repo.Name())
|
||||
|
||||
log.Info().Msg("Running git gc for repository " + repoPath)
|
||||
|
||||
cmd := exec.Command("git", "gc")
|
||||
cmd.Dir = repoPath
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Cannot run git gc for repository " + repoPath)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func HasNoCommits(user string, gist string) (bool, error) {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
cmd := exec.Command("git", "rev-parse", "--all")
|
||||
cmd.Dir = repositoryPath
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if out.String() == "" {
|
||||
return true, nil // No commits exist
|
||||
}
|
||||
|
||||
return false, nil // Commits exist
|
||||
}
|
||||
|
||||
func GetGitVersion() (string, error) {
|
||||
cmd := exec.Command("git", "--version")
|
||||
stdout, err := cmd.Output()
|
||||
@@ -270,19 +516,33 @@ func GetGitVersion() (string, error) {
|
||||
return versionFields[2], nil
|
||||
}
|
||||
|
||||
func copyFiles(repositoryPath string) error {
|
||||
func CreateDotGitFiles(user string, gist string) error {
|
||||
repositoryPath := RepositoryPath(user, gist)
|
||||
|
||||
f1, err := os.OpenFile(filepath.Join(repositoryPath, "git-daemon-export-ok"), os.O_RDONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f1.Close()
|
||||
|
||||
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", "pre-receive"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744)
|
||||
if os.Getenv("OPENGIST_SKIP_GIT_HOOKS") != "1" {
|
||||
for _, hook := range []string{"pre-receive", "post-receive"} {
|
||||
if err = createDotGitHookFile(repositoryPath, hook, fmt.Sprintf(hookTemplate, hook)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDotGitHookFile(repositoryPath string, hook string, content string) error {
|
||||
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0744)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = preReceiveDst.WriteString(preReceive); err != nil {
|
||||
if _, err = preReceiveDst.WriteString(content); err != nil {
|
||||
return err
|
||||
}
|
||||
defer preReceiveDst.Close()
|
||||
@@ -290,29 +550,21 @@ func copyFiles(repositoryPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const preReceive = `#!/bin/sh
|
||||
func removeFilesExceptGit(dir string) error {
|
||||
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() && filepath.Base(path) == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if !d.IsDir() {
|
||||
return os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
disallowed_files=""
|
||||
|
||||
while read -r old_rev new_rev ref
|
||||
do
|
||||
while IFS= read -r file
|
||||
do
|
||||
case $file in
|
||||
*/*)
|
||||
disallowed_files="${disallowed_files}${file} "
|
||||
;;
|
||||
esac
|
||||
done <<EOF
|
||||
$(git diff --name-only "$old_rev" "$new_rev")
|
||||
EOF
|
||||
done
|
||||
|
||||
if [ -n "$disallowed_files" ]; then
|
||||
echo "Pushing files in folders is not allowed:"
|
||||
for file in $disallowed_files; do
|
||||
echo " $file"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
const hookTemplate = `#!/bin/sh
|
||||
"$OG_OPENGIST_HOME_INTERNAL/symlinks/opengist" --config=$OG_OPENGIST_HOME_INTERNAL/symlinks/config.yml hook %s
|
||||
`
|
||||
|
||||
242
internal/git/commands_test.go
Normal file
242
internal/git/commands_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInitDeleteRepository(t *testing.T) {
|
||||
SetupTest(t)
|
||||
defer TeardownTest(t)
|
||||
|
||||
cmd := exec.Command("git", "rev-parse", "--is-bare-repository")
|
||||
cmd.Dir = RepositoryPath("thomas", "gist1")
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err, "Could not run git command")
|
||||
require.Equal(t, "true", strings.TrimSpace(string(out)), "Repository is not bare")
|
||||
|
||||
_, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "git-daemon-export-ok"))
|
||||
require.NoError(t, err, "git-daemon-export-ok file not found")
|
||||
|
||||
err = DeleteRepository("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not delete repository")
|
||||
require.NoDirExists(t, RepositoryPath("thomas", "gist1"), "Repository should not exist")
|
||||
}
|
||||
|
||||
func TestCommits(t *testing.T) {
|
||||
SetupTest(t)
|
||||
defer TeardownTest(t)
|
||||
|
||||
hasNoCommits, err := HasNoCommits("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not check if repository has no commits")
|
||||
require.True(t, hasNoCommits, "Repository should have no commits")
|
||||
|
||||
CommitToBare(t, "thomas", "gist1", nil)
|
||||
|
||||
hasNoCommits, err = HasNoCommits("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not check if repository has no commits")
|
||||
require.False(t, hasNoCommits, "Repository should have commits")
|
||||
|
||||
nbCommits, err := CountCommits("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not count commits")
|
||||
require.Equal(t, "1", nbCommits, "Repository should have 1 commit")
|
||||
|
||||
CommitToBare(t, "thomas", "gist1", nil)
|
||||
nbCommits, err = CountCommits("thomas", "gist1")
|
||||
require.NoError(t, err, "Could not count commits")
|
||||
require.Equal(t, "2", nbCommits, "Repository should have 2 commits")
|
||||
}
|
||||
|
||||
func TestContent(t *testing.T) {
|
||||
SetupTest(t)
|
||||
defer TeardownTest(t)
|
||||
|
||||
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "I love Opengist\n",
|
||||
"my_other_file.txt": `I really
|
||||
hate Opengist`,
|
||||
"rip.txt": "byebye",
|
||||
})
|
||||
|
||||
files, err := GetFilesOfRepository("thomas", "gist1", "HEAD")
|
||||
require.NoError(t, err, "Could not get files of repository")
|
||||
require.Subset(t, []string{"my_file.txt", "my_other_file.txt", "rip.txt"}, files, "Files are not correct")
|
||||
|
||||
content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", false)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.False(t, truncated, "Content should not be truncated")
|
||||
require.Equal(t, "I love Opengist\n", content, "Content is not correct")
|
||||
|
||||
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.False(t, truncated, "Content should not be truncated")
|
||||
require.Equal(t, "I really\nhate Opengist", content, "Content is not correct")
|
||||
|
||||
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_renamed_file.txt": "I love Opengist\n",
|
||||
"my_other_file.txt": `I really
|
||||
like Opengist actually`,
|
||||
"new_file.txt": "Wait now there is a new file",
|
||||
})
|
||||
|
||||
files, err = GetFilesOfRepository("thomas", "gist1", "HEAD")
|
||||
require.NoError(t, err, "Could not get files of repository")
|
||||
require.Subset(t, []string{"my_renamed_file.txt", "my_other_file.txt", "new_file.txt"}, files, "Files are not correct")
|
||||
|
||||
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.False(t, truncated, "Content should not be truncated")
|
||||
require.Equal(t, "I really\nlike Opengist actually", content, "Content is not correct")
|
||||
|
||||
commits, err := GetLog("thomas", "gist1", 0)
|
||||
require.NoError(t, err, "Could not get log")
|
||||
require.Equal(t, 2, len(commits), "Commits count are not correct")
|
||||
require.Regexp(t, "[a-f0-9]{40}", commits[0].Hash, "Commit ID is not correct")
|
||||
require.Regexp(t, "[0-9]{10}", commits[0].Timestamp, "Commit timestamp is not correct")
|
||||
require.Equal(t, "thomas", commits[0].AuthorName, "Commit author name is not correct")
|
||||
require.Equal(t, "thomas@mail.com", commits[0].AuthorEmail, "Commit author email is not correct")
|
||||
require.Equal(t, "4 files changed, 2 insertions, 2 deletions", commits[0].Changed, "Commit author name is not correct")
|
||||
|
||||
require.Contains(t, commits[0].Files, File{
|
||||
Filename: "my_renamed_file.txt",
|
||||
OldFilename: "my_file.txt",
|
||||
Content: "",
|
||||
Truncated: false,
|
||||
IsCreated: false,
|
||||
IsDeleted: false,
|
||||
}, "File my_renamed_file.txt is not correct")
|
||||
|
||||
require.Contains(t, commits[0].Files, File{
|
||||
Filename: "rip.txt",
|
||||
OldFilename: "",
|
||||
Content: `@@ -1 +0,0 @@
|
||||
-byebye
|
||||
\ No newline at end of file
|
||||
`,
|
||||
Truncated: false,
|
||||
IsCreated: false,
|
||||
IsDeleted: true,
|
||||
}, "File rip.txt is not correct")
|
||||
|
||||
require.Contains(t, commits[0].Files, File{
|
||||
Filename: "my_other_file.txt",
|
||||
OldFilename: "my_other_file.txt",
|
||||
Content: `@@ -1,2 +1,2 @@
|
||||
I really
|
||||
-hate Opengist
|
||||
\ No newline at end of file
|
||||
+like Opengist actually
|
||||
\ No newline at end of file
|
||||
`,
|
||||
Truncated: false,
|
||||
IsCreated: false,
|
||||
IsDeleted: false,
|
||||
}, "File my_other_file.txt is not correct")
|
||||
|
||||
require.Contains(t, commits[0].Files, File{
|
||||
Filename: "new_file.txt",
|
||||
OldFilename: "",
|
||||
Content: `@@ -0,0 +1 @@
|
||||
+Wait now there is a new file
|
||||
\ No newline at end of file
|
||||
`,
|
||||
Truncated: false,
|
||||
IsCreated: true,
|
||||
IsDeleted: false,
|
||||
}, "File new_file.txt is not correct")
|
||||
|
||||
commitsSkip1, err := GetLog("thomas", "gist1", 1)
|
||||
require.NoError(t, err, "Could not get log")
|
||||
require.Equal(t, commitsSkip1[0], commits[1], "Commits skips are not correct")
|
||||
}
|
||||
|
||||
func TestGitGc(t *testing.T) {
|
||||
SetupTest(t)
|
||||
defer TeardownTest(t)
|
||||
|
||||
err := GcRepos()
|
||||
require.NoError(t, err, "Could not run git gc")
|
||||
}
|
||||
|
||||
func TestFork(t *testing.T) {
|
||||
SetupTest(t)
|
||||
defer TeardownTest(t)
|
||||
|
||||
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "I love Opengist\n",
|
||||
})
|
||||
|
||||
err := ForkClone("thomas", "gist1", "thomas", "gist2")
|
||||
require.NoError(t, err, "Could not fork repository")
|
||||
|
||||
files1, err := GetFilesOfRepository("thomas", "gist1", "HEAD")
|
||||
require.NoError(t, err, "Could not get files of repository")
|
||||
files2, err := GetFilesOfRepository("thomas", "gist2", "HEAD")
|
||||
require.NoError(t, err, "Could not get files of repository")
|
||||
|
||||
require.Equal(t, files1, files2, "Files are not the same")
|
||||
|
||||
}
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
SetupTest(t)
|
||||
defer TeardownTest(t)
|
||||
|
||||
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "A",
|
||||
})
|
||||
|
||||
content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.False(t, truncated, "Content should not be truncated")
|
||||
require.Equal(t, 1, len(content), "Content size is not correct")
|
||||
|
||||
var builder strings.Builder
|
||||
for i := 0; i < truncateLimit+10; i++ {
|
||||
builder.WriteString("A")
|
||||
}
|
||||
str := builder.String()
|
||||
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": str,
|
||||
})
|
||||
|
||||
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.True(t, truncated, "Content should be truncated")
|
||||
require.Equal(t, truncateLimit, len(content), "Content size should be at truncate limit")
|
||||
|
||||
CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "AA\n" + str,
|
||||
})
|
||||
|
||||
content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true)
|
||||
require.NoError(t, err, "Could not get content")
|
||||
require.True(t, truncated, "Content should be truncated")
|
||||
require.Equal(t, 2, len(content), "Content size is not correct")
|
||||
}
|
||||
|
||||
func TestGitInitBranchNames(t *testing.T) {
|
||||
SetupTest(t)
|
||||
defer TeardownTest(t)
|
||||
|
||||
cmd := exec.Command("git", "symbolic-ref", "HEAD")
|
||||
cmd.Dir = RepositoryPath("thomas", "gist1")
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err, "Could not run git command")
|
||||
require.Equal(t, "refs/heads/master", strings.TrimSpace(string(out)), "Repository should have master branch as default")
|
||||
|
||||
config.C.GitDefaultBranch = "main"
|
||||
|
||||
err = InitRepository("thomas", "gist2")
|
||||
require.NoError(t, err)
|
||||
cmd = exec.Command("git", "symbolic-ref", "HEAD")
|
||||
cmd.Dir = RepositoryPath("thomas", "gist2")
|
||||
out, err = cmd.Output()
|
||||
require.NoError(t, err, "Could not run git command")
|
||||
require.Equal(t, "refs/heads/main", strings.TrimSpace(string(out)), "Repository should have main branch as default")
|
||||
}
|
||||
@@ -6,17 +6,18 @@ import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Filename string
|
||||
OldFilename string
|
||||
Content string
|
||||
Truncated bool
|
||||
IsCreated bool
|
||||
IsDeleted bool
|
||||
Filename string `json:"filename"`
|
||||
Size uint64 `json:"size"`
|
||||
HumanSize string `json:"human_size"`
|
||||
OldFilename string `json:"-"`
|
||||
Content string `json:"content"`
|
||||
Truncated bool `json:"truncated"`
|
||||
IsCreated bool `json:"-"`
|
||||
IsDeleted bool `json:"-"`
|
||||
}
|
||||
|
||||
type CsvFile struct {
|
||||
@@ -61,124 +62,287 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
|
||||
return string(buf), truncated, nil
|
||||
}
|
||||
|
||||
func parseLog(out io.Reader, maxBytes int) []*Commit {
|
||||
scanner := bufio.NewScanner(out)
|
||||
|
||||
// inspired from https://github.com/go-gitea/gitea/blob/main/services/gitdiff/gitdiff.go
|
||||
func parseLog(out io.Reader, maxFiles int, maxBytes int) ([]*Commit, error) {
|
||||
var commits []*Commit
|
||||
var currentCommit *Commit
|
||||
var currentFile *File
|
||||
var isContent bool
|
||||
var bytesRead = 0
|
||||
scanNext := true
|
||||
var headerParsed = false
|
||||
var skipped = false
|
||||
var line string
|
||||
var err error
|
||||
|
||||
input := bufio.NewReaderSize(out, maxBytes)
|
||||
|
||||
// Loop Commits
|
||||
loopLog:
|
||||
for {
|
||||
// If a commit was skipped, do not read a new line
|
||||
if !skipped {
|
||||
line, err = input.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break loopLog
|
||||
}
|
||||
return commits, err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing newline characters
|
||||
if len(line) > 0 && (line[len(line)-1] == '\n' || line[len(line)-1] == '\r') {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
|
||||
// Attempt to parse commit header (hash, author, mail, timestamp) or a diff
|
||||
switch line[0] {
|
||||
// Commit hash
|
||||
case 'c':
|
||||
if headerParsed {
|
||||
commits = append(commits, currentCommit)
|
||||
}
|
||||
skipped = false
|
||||
currentCommit = &Commit{Hash: line[2:], Files: []File{}}
|
||||
continue
|
||||
|
||||
// Author name
|
||||
case 'a':
|
||||
headerParsed = true
|
||||
currentCommit.AuthorName = line[2:]
|
||||
continue
|
||||
|
||||
// Author email
|
||||
case 'm':
|
||||
currentCommit.AuthorEmail = line[2:]
|
||||
continue
|
||||
|
||||
// Commit timestamp
|
||||
case 't':
|
||||
currentCommit.Timestamp = line[2:]
|
||||
continue
|
||||
|
||||
// Commit shortstat
|
||||
case ' ':
|
||||
changed := []byte(line)[1:]
|
||||
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
|
||||
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
|
||||
currentCommit.Changed = string(changed)
|
||||
|
||||
// shortstat is followed by an empty line
|
||||
line, err = input.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break loopLog
|
||||
}
|
||||
return commits, err
|
||||
}
|
||||
continue
|
||||
|
||||
// Commit diff
|
||||
default:
|
||||
// Loop files in diff
|
||||
loopCommit:
|
||||
for {
|
||||
// If we have reached the maximum number of files to show for a single commit, skip to the next commit
|
||||
if len(currentCommit.Files) >= maxFiles {
|
||||
line, err = skipToNextCommit(input)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break loopLog
|
||||
}
|
||||
return commits, err
|
||||
}
|
||||
|
||||
// Skip to the next commit
|
||||
headerParsed = false
|
||||
skipped = true
|
||||
break loopCommit
|
||||
}
|
||||
|
||||
// Else create a new file and parse it
|
||||
currentFile = &File{}
|
||||
parseRename := true
|
||||
|
||||
loopFileDiff:
|
||||
for {
|
||||
line, err = input.ReadString('\n')
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return commits, err
|
||||
}
|
||||
headerParsed = false
|
||||
break loopCommit
|
||||
}
|
||||
|
||||
// If the line is a newline character, the commit is finished
|
||||
if line == "\n" {
|
||||
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||
headerParsed = false
|
||||
break loopCommit
|
||||
}
|
||||
|
||||
// Attempt to parse the file header
|
||||
switch {
|
||||
case strings.HasPrefix(line, "diff --git"):
|
||||
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||
headerParsed = false
|
||||
break loopFileDiff
|
||||
case strings.HasPrefix(line, "old mode"):
|
||||
case strings.HasPrefix(line, "new mode"):
|
||||
case strings.HasPrefix(line, "index"):
|
||||
case strings.HasPrefix(line, "similarity index"):
|
||||
case strings.HasPrefix(line, "dissimilarity index"):
|
||||
continue
|
||||
case strings.HasPrefix(line, "rename from "):
|
||||
currentFile.OldFilename = line[12 : len(line)-1]
|
||||
case strings.HasPrefix(line, "rename to "):
|
||||
currentFile.Filename = line[10 : len(line)-1]
|
||||
parseRename = false
|
||||
case strings.HasPrefix(line, "copy from "):
|
||||
currentFile.OldFilename = line[10 : len(line)-1]
|
||||
case strings.HasPrefix(line, "copy to "):
|
||||
currentFile.Filename = line[8 : len(line)-1]
|
||||
parseRename = false
|
||||
case strings.HasPrefix(line, "new file"):
|
||||
currentFile.IsCreated = true
|
||||
case strings.HasPrefix(line, "deleted file"):
|
||||
currentFile.IsDeleted = true
|
||||
case strings.HasPrefix(line, "--- "):
|
||||
name := line[4 : len(line)-1]
|
||||
if parseRename && currentFile.IsDeleted {
|
||||
currentFile.Filename = name[2:]
|
||||
} else if parseRename && strings.HasPrefix(name, "a/") {
|
||||
currentFile.OldFilename = name[2:]
|
||||
}
|
||||
case strings.HasPrefix(line, "+++ "):
|
||||
name := line[4 : len(line)-1]
|
||||
if parseRename && strings.HasPrefix(name, "b/") {
|
||||
currentFile.Filename = name[2:]
|
||||
}
|
||||
|
||||
// Header is finally parsed, now we can parse the file diff content
|
||||
lineBytes, isFragment, err := parseDiffContent(currentFile, maxBytes, input)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return commits, err
|
||||
}
|
||||
|
||||
// EOF reached, commit is finished
|
||||
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||
headerParsed = false
|
||||
break loopCommit
|
||||
}
|
||||
|
||||
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||
|
||||
if string(lineBytes) == "" {
|
||||
headerParsed = false
|
||||
break loopCommit
|
||||
}
|
||||
|
||||
for isFragment {
|
||||
_, isFragment, err = input.ReadLine()
|
||||
if err != nil {
|
||||
return commits, fmt.Errorf("unable to ReadLine: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
break loopFileDiff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
commits = append(commits, currentCommit)
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func parseDiffContent(currentFile *File, maxBytes int, input *bufio.Reader) (lineBytes []byte, isFragment bool, err error) {
|
||||
sb := &strings.Builder{}
|
||||
var currFileLineCount int
|
||||
|
||||
for {
|
||||
if scanNext && !scanner.Scan() {
|
||||
break
|
||||
for isFragment {
|
||||
currentFile.Truncated = true
|
||||
|
||||
// Read the next line
|
||||
_, isFragment, err = input.ReadLine()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
scanNext = true
|
||||
|
||||
// new commit found
|
||||
currentFile = nil
|
||||
currentCommit = &Commit{Hash: string(scanner.Bytes()[2:]), Files: []File{}}
|
||||
sb.Reset()
|
||||
|
||||
scanner.Scan()
|
||||
currentCommit.AuthorName = string(scanner.Bytes()[2:])
|
||||
// Read the next line
|
||||
lineBytes, isFragment, err = input.ReadLine()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return lineBytes, isFragment, err
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
currentCommit.AuthorEmail = string(scanner.Bytes()[2:])
|
||||
// End of file
|
||||
if len(lineBytes) == 0 {
|
||||
return lineBytes, false, err
|
||||
}
|
||||
if lineBytes[0] == 'd' {
|
||||
return lineBytes, false, err
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
currentCommit.Timestamp = string(scanner.Bytes()[2:])
|
||||
|
||||
scanner.Scan()
|
||||
|
||||
// if there is no shortstat, it means that the commit is empty, we add it and move onto the next one
|
||||
if scanner.Bytes()[0] != ' ' {
|
||||
commits = append(commits, currentCommit)
|
||||
|
||||
// avoid scanning the next line, as we already did it
|
||||
scanNext = false
|
||||
if currFileLineCount >= maxBytes {
|
||||
currentFile.Truncated = true
|
||||
continue
|
||||
}
|
||||
|
||||
changed := scanner.Bytes()[1:]
|
||||
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
|
||||
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
|
||||
currentCommit.Changed = string(changed)
|
||||
|
||||
// twice because --shortstat adds a new line
|
||||
scanner.Scan()
|
||||
scanner.Scan()
|
||||
// commit header parsed
|
||||
|
||||
// files changes inside the commit
|
||||
for {
|
||||
line := scanner.Bytes()
|
||||
|
||||
// end of content of file
|
||||
if len(line) == 0 {
|
||||
isContent = false
|
||||
if currentFile != nil {
|
||||
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// new file found
|
||||
if bytes.HasPrefix(line, []byte("diff --git")) {
|
||||
// current file is finished, we can add it to the commit
|
||||
if currentFile != nil {
|
||||
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||
}
|
||||
|
||||
// create a new file
|
||||
isContent = false
|
||||
bytesRead = 0
|
||||
currentFile = &File{}
|
||||
filenameRegex := regexp.MustCompile(`^diff --git a/(.+) b/(.+)$`)
|
||||
matches := filenameRegex.FindStringSubmatch(string(line))
|
||||
if len(matches) == 3 {
|
||||
currentFile.Filename = matches[2]
|
||||
if matches[1] != matches[2] {
|
||||
currentFile.OldFilename = matches[1]
|
||||
}
|
||||
}
|
||||
scanner.Scan()
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(line, []byte("new")) {
|
||||
currentFile.IsCreated = true
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(line, []byte("deleted")) {
|
||||
currentFile.IsDeleted = true
|
||||
}
|
||||
|
||||
// file content found
|
||||
if line[0] == '@' {
|
||||
isContent = true
|
||||
}
|
||||
|
||||
if isContent {
|
||||
currentFile.Content += string(line) + "\n"
|
||||
|
||||
bytesRead += len(line)
|
||||
if bytesRead > maxBytes {
|
||||
currentFile.Truncated = true
|
||||
currentFile.Content = ""
|
||||
isContent = false
|
||||
line := string(lineBytes)
|
||||
if isFragment {
|
||||
currentFile.Truncated = true
|
||||
for isFragment {
|
||||
lineBytes, isFragment, err = input.ReadLine()
|
||||
if err != nil {
|
||||
return lineBytes, isFragment, fmt.Errorf("unable to ReadLine: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scanner.Scan()
|
||||
}
|
||||
|
||||
commits = append(commits, currentCommit)
|
||||
|
||||
if len(line) > maxBytes {
|
||||
currentFile.Truncated = true
|
||||
line = line[:maxBytes]
|
||||
}
|
||||
currentFile.Content += line + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
return commits
|
||||
func skipToNextCommit(input *bufio.Reader) (line string, err error) {
|
||||
// need to skip until the next cmdDiffHead
|
||||
var isFragment, wasFragment bool
|
||||
var lineBytes []byte
|
||||
for {
|
||||
lineBytes, isFragment, err = input.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if wasFragment {
|
||||
wasFragment = isFragment
|
||||
continue
|
||||
}
|
||||
if bytes.HasPrefix(lineBytes, []byte("c")) {
|
||||
break
|
||||
}
|
||||
wasFragment = isFragment
|
||||
}
|
||||
line = string(lineBytes)
|
||||
if isFragment {
|
||||
var tail string
|
||||
tail, err = input.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
line += tail
|
||||
}
|
||||
return line, err
|
||||
}
|
||||
|
||||
func ParseCsv(file *File) (*CsvFile, error) {
|
||||
|
||||
71
internal/git/test_funcs.go
Normal file
71
internal/git/test_funcs.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func SetupTest(t *testing.T) {
|
||||
_ = os.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1")
|
||||
|
||||
err := config.InitConfig("", io.Discard)
|
||||
require.NoError(t, err, "Could not init config")
|
||||
|
||||
err = os.MkdirAll(path.Join(config.GetHomeDir(), "tests"), 0755)
|
||||
ReposDirectory = path.Join("tests")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(config.GetHomeDir(), "tmp", "repos"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = InitRepository("thomas", "gist1")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TeardownTest(t *testing.T) {
|
||||
err := os.RemoveAll(path.Join(config.GetHomeDir(), "tests"))
|
||||
require.NoError(t, err, "Could not remove repos directory")
|
||||
}
|
||||
|
||||
func CommitToBare(t *testing.T, user string, gist string, files map[string]string) {
|
||||
err := CloneTmp(user, gist, gist, "thomas@mail.com", true)
|
||||
require.NoError(t, err, "Could not clone repository")
|
||||
|
||||
if len(files) > 0 {
|
||||
for filename, content := range files {
|
||||
if strings.Contains(filename, "/") {
|
||||
dir := filepath.Dir(filename)
|
||||
err := os.MkdirAll(filepath.Join(TmpRepositoryPath(gist), dir), os.ModePerm)
|
||||
require.NoError(t, err, "Could not create directory")
|
||||
}
|
||||
_ = os.WriteFile(filepath.Join(TmpRepositoryPath(gist), filename), []byte(content), 0644)
|
||||
|
||||
if err := AddAll(gist); err != nil {
|
||||
require.NoError(t, err, "Could not add all to repository")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := CommitRepository(gist, user, "thomas@mail.com"); err != nil {
|
||||
require.NoError(t, err, "Could not commit to repository")
|
||||
}
|
||||
|
||||
if err := Push(gist); err != nil {
|
||||
require.NoError(t, err, "Could not push to repository")
|
||||
}
|
||||
}
|
||||
|
||||
func LastHashOfCommit(t *testing.T, user string, gist string) string {
|
||||
cmd := exec.Command("git", "rev-parse", "HEAD")
|
||||
cmd.Dir = RepositoryPath(user, gist)
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err, "Could not run git command")
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
24
internal/hooks/hook.go
Normal file
24
internal/hooks/hook.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const BaseHash = "0000000000000000000000000000000000000000"
|
||||
|
||||
func pushOptions() map[string]string {
|
||||
opts := make(map[string]string)
|
||||
if pushCount, err := strconv.Atoi(os.Getenv("GIT_PUSH_OPTION_COUNT")); err == nil {
|
||||
for i := 0; i < pushCount; i++ {
|
||||
opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", i))
|
||||
kv := strings.SplitN(opt, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
opts[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return opts
|
||||
}
|
||||
107
internal/hooks/post_receive.go
Normal file
107
internal/hooks/post_receive.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func PostReceive(in io.Reader, out, er io.Writer) error {
|
||||
var outputSb strings.Builder
|
||||
newGist := false
|
||||
opts := pushOptions()
|
||||
gistUrl := os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL")
|
||||
validator := utils.NewValidator()
|
||||
|
||||
scanner := bufio.NewScanner(in)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) != 3 {
|
||||
_, _ = fmt.Fprintln(er, "Invalid input")
|
||||
return fmt.Errorf("invalid input")
|
||||
}
|
||||
oldrev, _, refname := parts[0], parts[1], parts[2]
|
||||
|
||||
if err := verifyHEAD(); err != nil {
|
||||
setSymbolicRef(refname)
|
||||
}
|
||||
|
||||
if oldrev == BaseHash {
|
||||
newGist = true
|
||||
}
|
||||
}
|
||||
|
||||
gist, err := db.GetGistByID(os.Getenv("OPENGIST_REPOSITORY_ID"))
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(er, "Failed to get gist")
|
||||
return fmt.Errorf("failed to get gist: %w", err)
|
||||
}
|
||||
|
||||
if slices.Contains([]string{"public", "unlisted", "private"}, opts["visibility"]) {
|
||||
gist.Private, _ = db.ParseVisibility(opts["visibility"])
|
||||
outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"]))
|
||||
}
|
||||
|
||||
if opts["url"] != "" && validator.Var(opts["url"], "max=32,alphanumdashorempty") == nil {
|
||||
gist.URL = opts["url"]
|
||||
lastIndex := strings.LastIndex(gistUrl, "/")
|
||||
gistUrl = gistUrl[:lastIndex+1] + gist.URL
|
||||
if !newGist {
|
||||
outputSb.WriteString(fmt.Sprintf("Gist URL set to %s. Set the Git remote URL via:\n", gistUrl))
|
||||
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
|
||||
}
|
||||
}
|
||||
|
||||
if opts["title"] != "" && validator.Var(opts["title"], "max=250") == nil {
|
||||
gist.Title = opts["title"]
|
||||
outputSb.WriteString(fmt.Sprintf("Gist title set to \"%s\"\n\n", opts["title"]))
|
||||
}
|
||||
|
||||
if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil {
|
||||
_, _ = fmt.Fprintln(er, "Failed to check if gist has no commits")
|
||||
return fmt.Errorf("failed to check if gist has no commits: %w", err)
|
||||
} else if hasNoCommits {
|
||||
if err = gist.Delete(); err != nil {
|
||||
_, _ = fmt.Fprintln(er, "Failed to delete gist")
|
||||
return fmt.Errorf("failed to delete gist: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_ = gist.SetLastActiveNow()
|
||||
err = gist.UpdatePreviewAndCount(true)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(er, "Failed to update gist")
|
||||
return fmt.Errorf("failed to update gist: %w", err)
|
||||
}
|
||||
|
||||
gist.AddInIndex()
|
||||
|
||||
if newGist {
|
||||
outputSb.WriteString(fmt.Sprintf("Your new gist has been created here: %s\n", gistUrl))
|
||||
outputSb.WriteString("If you want to keep working with your gist, you could set the Git remote URL via:\n")
|
||||
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
|
||||
}
|
||||
|
||||
outputStr := outputSb.String()
|
||||
if outputStr != "" {
|
||||
_, _ = fmt.Fprint(out, "\n"+outputStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyHEAD() error {
|
||||
return exec.Command("git", "rev-parse", "--verify", "--quiet", "HEAD").Run()
|
||||
}
|
||||
|
||||
func setSymbolicRef(refname string) {
|
||||
_ = exec.Command("git", "symbolic-ref", "HEAD", refname).Run()
|
||||
}
|
||||
78
internal/hooks/pre_receive.go
Normal file
78
internal/hooks/pre_receive.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func PreReceive(in io.Reader, out, er io.Writer) error {
|
||||
var err error
|
||||
var disallowedFiles []string
|
||||
var disallowedCommits []string
|
||||
|
||||
scanner := bufio.NewScanner(in)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.Split(line, " ")
|
||||
if len(parts) < 3 {
|
||||
_, _ = fmt.Fprintln(er, "Invalid input")
|
||||
return fmt.Errorf("invalid input")
|
||||
}
|
||||
|
||||
oldRev, newRev := parts[0], parts[1]
|
||||
|
||||
var changedFiles string
|
||||
if oldRev == BaseHash {
|
||||
// First commit
|
||||
if changedFiles, err = getChangedFiles(newRev); err != nil {
|
||||
_, _ = fmt.Fprintln(er, "Failed to get changed files")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if changedFiles, err = getChangedFiles(fmt.Sprintf("%s..%s", oldRev, newRev)); err != nil {
|
||||
_, _ = fmt.Fprintln(er, "Failed to get changed files")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var currentCommit string
|
||||
for _, file := range strings.Fields(changedFiles) {
|
||||
if strings.HasPrefix(file, "/") {
|
||||
currentCommit = file[1:]
|
||||
}
|
||||
|
||||
if strings.Contains(file[1:], "/") {
|
||||
disallowedFiles = append(disallowedFiles, file)
|
||||
disallowedCommits = append(disallowedCommits, currentCommit[0:7])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(disallowedFiles) > 0 {
|
||||
_, _ = fmt.Fprintln(out, "\nPushing files in directories is not allowed:")
|
||||
for i := range disallowedFiles {
|
||||
_, _ = fmt.Fprintf(out, " %s (%s)\n", disallowedFiles[i], disallowedCommits[i])
|
||||
}
|
||||
_, _ = fmt.Fprintln(out)
|
||||
return fmt.Errorf("pushing files in directories is not allowed: %s", disallowedFiles)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getChangedFiles(rev string) (string, error) {
|
||||
cmd := exec.Command("git", "log", "--name-only", "--format=/%H", "--diff-filter=AM", rev)
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return out.String(), nil
|
||||
}
|
||||
54
internal/hooks/pre_receive_test.go
Normal file
54
internal/hooks/pre_receive_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPreReceiveHook(t *testing.T) {
|
||||
git.SetupTest(t)
|
||||
defer git.TeardownTest(t)
|
||||
var lastCommitHash string
|
||||
err := os.Chdir(git.RepositoryPath("thomas", "gist1"))
|
||||
require.NoError(t, err, "Could not change directory")
|
||||
|
||||
git.CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "some allowed file",
|
||||
"my_file2.txt": "some allowed file\nagain",
|
||||
})
|
||||
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
|
||||
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
|
||||
require.NoError(t, err, "Should not have an error on pre-receive hook for commit+push 1")
|
||||
|
||||
git.CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "some allowed file",
|
||||
"dir/my_file.txt": "some disallowed file suddenly",
|
||||
})
|
||||
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
|
||||
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
|
||||
require.Error(t, err, "Should have an error on pre-receive hook for commit+push 2")
|
||||
require.Equal(t, "pushing files in directories is not allowed: [dir/my_file.txt]", err.Error(), "Error message is not correct")
|
||||
|
||||
git.CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"my_file.txt": "some allowed file",
|
||||
"dir/ok/afileagain.txt": "some disallowed file\nagain",
|
||||
})
|
||||
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
|
||||
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
|
||||
require.Error(t, err, "Should have an error on pre-receive hook for commit+push 3")
|
||||
require.Equal(t, "pushing files in directories is not allowed: [dir/ok/afileagain.txt dir/my_file.txt]", err.Error(), "Error message is not correct")
|
||||
|
||||
git.CommitToBare(t, "thomas", "gist1", map[string]string{
|
||||
"allowedfile.txt": "some allowed file only",
|
||||
})
|
||||
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
|
||||
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
|
||||
require.Error(t, err, "Should have an error on pre-receive hook for commit+push 4")
|
||||
require.Equal(t, "pushing files in directories is not allowed: [dir/ok/afileagain.txt dir/my_file.txt]", err.Error(), "Error message is not correct")
|
||||
|
||||
_ = os.Chdir(os.TempDir()) // Leave the current dir to avoid errors on teardown
|
||||
}
|
||||
141
internal/i18n/locale.go
Normal file
141
internal/i18n/locale.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/thomiceli/opengist/internal/i18n/locales"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/language/display"
|
||||
"gopkg.in/yaml.v3"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var title = cases.Title(language.English)
|
||||
var Locales = NewLocaleStore()
|
||||
|
||||
type LocaleStore struct {
|
||||
Locales map[string]*Locale
|
||||
}
|
||||
|
||||
type Locale struct {
|
||||
Code string
|
||||
Name string
|
||||
Messages map[string]string
|
||||
}
|
||||
|
||||
// NewLocaleStore creates a new LocaleStore
|
||||
func NewLocaleStore() *LocaleStore {
|
||||
return &LocaleStore{
|
||||
Locales: make(map[string]*Locale),
|
||||
}
|
||||
}
|
||||
|
||||
// loadLocaleFromYAML loads a single Locale from a given YAML file
|
||||
func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error {
|
||||
a, err := locales.Files.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := io.ReadAll(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag, err := language.Parse(localeCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := display.Self.Name(tag)
|
||||
if tag == language.AmericanEnglish {
|
||||
name = "English"
|
||||
} else if tag == language.EuropeanSpanish {
|
||||
name = "Español"
|
||||
}
|
||||
|
||||
locale := &Locale{
|
||||
Code: localeCode,
|
||||
Name: title.String(name),
|
||||
Messages: make(map[string]string),
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(data, &locale.Messages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store.Locales[localeCode] = locale
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *LocaleStore) LoadAll() error {
|
||||
return fs.WalkDir(locales.Files, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
localeKey := strings.TrimSuffix(path, filepath.Ext(path))
|
||||
err := store.loadLocaleFromYAML(localeKey, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (store *LocaleStore) GetLocale(lang string) (*Locale, error) {
|
||||
_, ok := store.Locales[lang]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("locale %s not found", lang)
|
||||
}
|
||||
|
||||
return store.Locales[lang], nil
|
||||
}
|
||||
|
||||
func (store *LocaleStore) HasLocale(lang string) bool {
|
||||
_, ok := store.Locales[lang]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (store *LocaleStore) MatchTag(langs []language.Tag) string {
|
||||
for _, lang := range langs {
|
||||
if store.HasLocale(lang.String()) {
|
||||
return lang.String()
|
||||
}
|
||||
}
|
||||
|
||||
return "en-US"
|
||||
}
|
||||
|
||||
func (l *Locale) String(key string, args ...any) string {
|
||||
message := l.Messages[key]
|
||||
|
||||
if message == "" {
|
||||
return Locales.Locales["en-US"].String(key, args...)
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return message
|
||||
}
|
||||
|
||||
return fmt.Sprintf(message, args...)
|
||||
}
|
||||
|
||||
func (l *Locale) Tr(key string, args ...any) template.HTML {
|
||||
message := l.Messages[key]
|
||||
|
||||
if message == "" {
|
||||
return Locales.Locales["en-US"].Tr(key, args...)
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return template.HTML(message)
|
||||
}
|
||||
|
||||
return template.HTML(fmt.Sprintf(message, args...))
|
||||
}
|
||||
261
internal/i18n/locales/cs-CZ.yml
Normal file
261
internal/i18n/locales/cs-CZ.yml
Normal file
@@ -0,0 +1,261 @@
|
||||
gist.public: Veřejný
|
||||
gist.unlisted: Neveřejný
|
||||
gist.private: Privátní
|
||||
|
||||
gist.header.like: To se mi líbí
|
||||
gist.header.unlike: Už se mi nelíbí
|
||||
gist.header.fork: Fork
|
||||
gist.header.edit: Upravit
|
||||
gist.header.delete: Smazat
|
||||
gist.header.forked-from: Forkováno z
|
||||
gist.header.last-active: Naposledy aktivní
|
||||
gist.header.select-tab: Vyberte záložku
|
||||
gist.header.code: Kód
|
||||
gist.header.revisions: Revize
|
||||
gist.header.revision: Revize
|
||||
gist.header.clone-http: Klonovat pomocí %s
|
||||
gist.header.clone-http-help: Klonovat s pomocí Git pomocí základní autentizace HTTP.
|
||||
gist.header.clone-ssh: Klonovat pomocí SSH
|
||||
gist.header.clone-ssh-help: Klonovat s pomocí Git pomocí klíče SSH.
|
||||
gist.header.embed: ''
|
||||
gist.header.embed-help: ''
|
||||
gist.header.download-zip: Stáhnout ZIP
|
||||
|
||||
gist.raw: Raw
|
||||
gist.file-truncated: Tento soubor byl zkrácen.
|
||||
gist.watch-full-file: Zobrazit celý soubor.
|
||||
gist.file-not-valid: Tento soubor není validní CSV.
|
||||
gist.no-content: Žádný obsah
|
||||
|
||||
gist.new.new_gist: Nový gist
|
||||
gist.new.title: Titulek
|
||||
gist.new.description: Popis
|
||||
gist.new.filename-with-extension: Název s příponou
|
||||
gist.new.indent-mode: Režim odsazení
|
||||
gist.new.indent-mode-space: Mezery
|
||||
gist.new.indent-mode-tab: Tabulátory
|
||||
gist.new.indent-size: Velikost odsazení
|
||||
gist.new.wrap-mode: Režim zalamování
|
||||
gist.new.wrap-mode-no: Bez zalamování
|
||||
gist.new.wrap-mode-soft: Měkké zalamování
|
||||
gist.new.add-file: Přidat soubor
|
||||
gist.new.create-public-button: Vytvořit veřejný gist
|
||||
gist.new.create-unlisted-button: Vytvořit neveřejný gist
|
||||
gist.new.create-private-button: Vytvořit soukromý gist
|
||||
|
||||
gist.edit.editing: Úprava
|
||||
gist.edit.change-visibility: Změnit viditelnost
|
||||
gist.edit.delete: Smazat
|
||||
gist.edit.cancel: Zrušit
|
||||
gist.edit.save: Uložit
|
||||
|
||||
gist.list.joined: Připojeno
|
||||
gist.list.all: Všechny gisty
|
||||
gist.list.search-results: Výsledky hledání
|
||||
gist.list.sort: Seřadit
|
||||
gist.list.sort-by-created: Vytvořeno
|
||||
gist.list.sort-by-updated: Aktualizováno
|
||||
gist.list.order-by-asc: Nejméně nedávno
|
||||
gist.list.order-by-desc: Nedávno
|
||||
gist.list.select-tab: Vyberte záložku
|
||||
gist.list.liked: Líbí se
|
||||
gist.list.likes: Lajky
|
||||
gist.list.forked: Forkováno
|
||||
gist.list.forked-from: Forkováno z
|
||||
gist.list.forks: Forky
|
||||
gist.list.files: Soubory
|
||||
gist.list.last-active: Naposledy aktivní
|
||||
gist.list.no-gists: Žádné gisty
|
||||
|
||||
gist.forks: Forky
|
||||
gist.forks.view: Zobrazit forky
|
||||
gist.forks.no: Žádné veřejné forky
|
||||
|
||||
|
||||
gist.likes: Lajky
|
||||
gist.likes.no: Zatím žádné lajky
|
||||
|
||||
gist.revisions: Revize
|
||||
gist.revision.revised: revidoval tento gist
|
||||
gist.revision.go-to-revision: Přejít na revizi
|
||||
gist.revision.file-created: vytvořil soubor
|
||||
gist.revision.file-deleted: smazal soubor
|
||||
gist.revision.file-renamed: přejmenováno na
|
||||
gist.revision.diff-truncated: Diff je příliš velký na zobrazení
|
||||
gist.revision.file-renamed-no-changes: Soubor přejmenován beze změn
|
||||
gist.revision.empty-file: Prázdný soubor
|
||||
gist.revision.no-changes: Žádné změny
|
||||
gist.revision.no-revisions: Žádné revize k zobrazení
|
||||
|
||||
|
||||
settings: Nastavení
|
||||
settings.email: Email
|
||||
settings.email-help: Používá se pro commity a Gravatary
|
||||
settings.email-set: Nastavit email
|
||||
settings.link-accounts: Propojit účty
|
||||
settings.link-github-account: Propojit účet na GitHubu
|
||||
settings.link-gitea-account: Propojit účet na Gitea
|
||||
settings.unlink-github-account: Odpojit účet na GitHubu
|
||||
settings.unlink-gitea-account: Odpojit účet na Gitea
|
||||
settings.delete-account: Smazat účet
|
||||
settings.delete-account-confirm: Opravdu chcete smazat svůj účet?
|
||||
settings.add-ssh-key: Přidat SSH klíč
|
||||
settings.add-ssh-key-help: Používá se pouze k tahání/pushování gistů pomocí Gitu přes SSH
|
||||
settings.add-ssh-key-title: Titulek
|
||||
settings.add-ssh-key-content: Obsah
|
||||
settings.delete-ssh-key: Smazat
|
||||
settings.delete-ssh-key-confirm: Potvrdit smazání SSH klíče
|
||||
settings.ssh-key-added-at: Přidáno
|
||||
settings.ssh-key-never-used: Nikdy nepoužito
|
||||
settings.ssh-key-last-used: Naposledy použito
|
||||
settings.create-password: Vytvořit heslo
|
||||
settings.create-password-help: Vytvořte si heslo pro přihlášení do Opengist pomocí HTTP
|
||||
settings.change-password: Změnit heslo
|
||||
settings.change-password-help: Změňte své heslo pro přihlášení do Opengist pomocí HTTP
|
||||
settings.password-label-title: Heslo
|
||||
|
||||
auth.signup-disabled: Správce zakázal registraci
|
||||
auth.login: Přihlásit se
|
||||
auth.signup: Registrovat
|
||||
auth.new-account: Nový účet
|
||||
auth.username: Uživatelské jméno
|
||||
auth.password: Heslo
|
||||
auth.register-instead: Raději se zaregistrovat
|
||||
auth.login-instead: Raději se přihlásit
|
||||
auth.oauth: Pokračovat s účtem na %s
|
||||
|
||||
error: Chyba
|
||||
|
||||
header.menu.all: Všechno
|
||||
header.menu.new: Nové
|
||||
header.menu.search: Hledat
|
||||
header.menu.my-gists: Moje gisty
|
||||
header.menu.liked: Lajknuté
|
||||
header.menu.admin: Administrace
|
||||
header.menu.settings: Nastavení
|
||||
header.menu.logout: Odhlásit se
|
||||
header.menu.register: Registrovat
|
||||
header.menu.login: Přihlásit se
|
||||
header.menu.light: Světlý
|
||||
header.menu.dark: Tmavý
|
||||
header.menu.system: Systém
|
||||
footer.powered-by: Vytvořeno pomocí %s
|
||||
|
||||
pagination.older: Starší
|
||||
pagination.newer: Novější
|
||||
pagination.previous: Předchozí
|
||||
pagination.next: Další
|
||||
|
||||
admin.admin_panel: Administrační panel
|
||||
admin.general: Obecné
|
||||
admin.users: Uživatelé
|
||||
admin.gists: Gisty
|
||||
admin.configuration: Konfigurace
|
||||
admin.versions: Verze
|
||||
admin.ssh_keys: SSH klíče
|
||||
admin.stats: Statistiky
|
||||
admin.actions: Akce
|
||||
admin.actions.sync-fs: Synchronizovat gisty ze souborového systému
|
||||
admin.actions.sync-db: Synchronizovat gisty z databáze
|
||||
admin.actions.git-gc: Garbage collect git repozitářů
|
||||
admin.id: ID
|
||||
admin.user: Uživatel
|
||||
admin.delete: Smazat
|
||||
admin.created_at: Vytvořeno
|
||||
|
||||
admin.config-link: Tato konfigurace může být %s pomocí YAML konfiguračního souboru a/nebo prostřednictvím proměnných prostředí.
|
||||
admin.config-link-overriden: přepsána
|
||||
admin.disable-signup: Zakázat registraci
|
||||
admin.disable-signup_help: Zakázat vytváření nových účtů.
|
||||
admin.require-login: Vyžadovat přihlášení
|
||||
admin.require-login_help: Vynutit, aby uživatelé byli přihlášeni k zobrazení gistů.
|
||||
admin.disable-login: Zakázat přihlášení
|
||||
admin.disable-login_help: Zakázat přihlašování pomocí formuláře pro přihlášení a vynutit používání OAuth poskytovatele.
|
||||
admin.disable-gravatar: Zakázat Gravatar
|
||||
admin.disable-gravatar_help: Zakázat použití Gravataru jako poskytovatele avatara.
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: Opravdu chcete smazat tohoto uživatele?
|
||||
|
||||
admin.gists.title: Titulek
|
||||
admin.gists.private: Soukromé?
|
||||
admin.gists.nb-files: Počet souborů
|
||||
admin.gists.nb-likes: Počet lajků
|
||||
admin.gists.delete_confirm: Opravdu chcete smazat tento gist?
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.actions.reset-hooks: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
gist.new.create-a-new-gist: ''
|
||||
gist.edit.edit-gist: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-be-empty: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
gist.list.all-liked-by: ''
|
||||
gist.list.all-forked-by: ''
|
||||
gist.list.all-from: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
flash.gist.deleted: ''
|
||||
gist.new.url: ''
|
||||
gist.search.found: ''
|
||||
gist.search.no-results: ''
|
||||
gist.search.help.user: ''
|
||||
gist.search.help.title: ''
|
||||
gist.search.help.filename: ''
|
||||
gist.search.help.extension: ''
|
||||
gist.search.help.language: ''
|
||||
settings.change-username: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.actions.sync-previews: ''
|
||||
admin.actions.index-gists: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
gist.new.preview: ''
|
||||
settings.link-gitlab-account: ''
|
||||
settings.unlink-gitlab-account: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
260
internal/i18n/locales/de-DE.yml
Normal file
260
internal/i18n/locales/de-DE.yml
Normal file
@@ -0,0 +1,260 @@
|
||||
gist.public: 'Öffentlich'
|
||||
gist.unlisted: 'Nicht Gelistet'
|
||||
gist.private: 'Privat'
|
||||
|
||||
gist.header.like: 'Favorisieren'
|
||||
gist.header.unlike: 'Favorit entfernen'
|
||||
gist.header.fork: 'Fork'
|
||||
gist.header.edit: 'Bearbeiten'
|
||||
gist.header.delete: 'Löschen'
|
||||
gist.header.forked-from: 'Geforkt von'
|
||||
gist.header.last-active: 'Zuletzt aktiv'
|
||||
gist.header.select-tab: 'Tab auswählen'
|
||||
gist.header.code: 'Code'
|
||||
gist.header.revisions: 'Änderungen'
|
||||
gist.header.revision: 'Änderung'
|
||||
gist.header.clone-http: 'Mit %s Klonen'
|
||||
gist.header.clone-http-help: 'Mit Git und HTTP Basic Authentication Klonen.'
|
||||
gist.header.clone-ssh: 'Mit SSH Klonen'
|
||||
gist.header.clone-ssh-help: 'Mit Git und SSH Schlüssel Klonen.'
|
||||
gist.header.embed: 'Einbetten'
|
||||
gist.header.embed-help: 'Bette diese Gist in deine Webseite ein.'
|
||||
gist.header.download-zip: 'ZIP Herunterladen'
|
||||
|
||||
gist.raw: 'Orginalformat'
|
||||
gist.file-truncated: 'Diese Datei wurde abgeschnitten.'
|
||||
gist.watch-full-file: 'Die gesamte Datei anzeigen.'
|
||||
gist.file-not-valid: 'Diese Datei ist keine korrekte CSV Datei.'
|
||||
gist.no-content: 'Keine Dateien gefunden'
|
||||
|
||||
gist.new.new_gist: 'Neue Gist'
|
||||
gist.new.title: 'Titel'
|
||||
gist.new.description: 'Beschreibung'
|
||||
gist.new.url: 'URL'
|
||||
gist.new.filename-with-extension: 'Dateiname mit Erweiterung'
|
||||
gist.new.indent-mode: 'Einrückungs Modus'
|
||||
gist.new.indent-mode-space: 'Leerzeichen'
|
||||
gist.new.indent-mode-tab: 'Tab'
|
||||
gist.new.indent-size: 'Einrückungs Größe'
|
||||
gist.new.wrap-mode: 'Textumbruch Modus'
|
||||
gist.new.wrap-mode-no: 'kein Textumruch'
|
||||
gist.new.wrap-mode-soft: 'weicher Zeilenumbruch'
|
||||
gist.new.add-file: 'Datei hinzufügen'
|
||||
gist.new.create-public-button: 'Öffentliche Gist erstellen'
|
||||
gist.new.create-unlisted-button: 'Nicht gelistete Gist erstellen'
|
||||
gist.new.create-private-button: 'Private Gist erstellen'
|
||||
|
||||
gist.edit.editing: 'Bearbeiten'
|
||||
gist.edit.change-visibility: 'Sichtbarkeit ändern'
|
||||
gist.edit.delete: 'Löschen'
|
||||
gist.edit.cancel: 'Abbrechen'
|
||||
gist.edit.save: 'Speichern'
|
||||
|
||||
gist.list.joined: 'Gemeinsam'
|
||||
gist.list.all: 'Alle Gists'
|
||||
gist.list.search-results: 'Suchergebnisse'
|
||||
gist.list.sort: 'Sortieren'
|
||||
gist.list.sort-by-created: 'erstellt'
|
||||
gist.list.sort-by-updated: 'bearbeitet'
|
||||
gist.list.order-by-asc: 'Älteste'
|
||||
gist.list.order-by-desc: 'Neueste'
|
||||
gist.list.select-tab: 'Tab Auswählen'
|
||||
gist.list.liked: 'Favorisiert'
|
||||
gist.list.likes: 'Favoriten'
|
||||
gist.list.forked: 'Forked'
|
||||
gist.list.forked-from: 'Forked von'
|
||||
gist.list.forks: 'Forks'
|
||||
gist.list.files: 'Dateien'
|
||||
gist.list.last-active: 'Zuletzt aktiv'
|
||||
gist.list.no-gists: 'Keine Gists'
|
||||
|
||||
gist.search.found: 'Gists gefunden'
|
||||
gist.search.no-results: 'Keine Gists gefunden'
|
||||
gist.search.help.user: 'Gists erstellt von Nutzer'
|
||||
gist.search.help.title: 'Gists mit Titel'
|
||||
gist.search.help.filename: 'Gists mit Dateinamen'
|
||||
gist.search.help.extension: 'Gists mit Dateiendung'
|
||||
gist.search.help.language: 'Gists in Sprache'
|
||||
|
||||
gist.forks: 'Forks'
|
||||
gist.forks.view: 'Fork ansehen'
|
||||
gist.forks.no: 'Keine öffentlichen Forks'
|
||||
|
||||
gist.likes: 'Favoriten'
|
||||
gist.likes.no: 'Keine Favorisierungen'
|
||||
|
||||
gist.revisions: 'Revisionen'
|
||||
gist.revision.revised: 'hat die Gist bearbeitet'
|
||||
gist.revision.go-to-revision: 'Zu Änderung gehen'
|
||||
gist.revision.file-created: 'Datei erstellt'
|
||||
gist.revision.file-deleted: 'Datei gelöscht'
|
||||
gist.revision.file-renamed: 'umbenannt zu'
|
||||
gist.revision.diff-truncated: 'Diff zu groß um angezeigt zu werden'
|
||||
gist.revision.file-renamed-no-changes: 'Datei ohne Änderung umbenannt'
|
||||
gist.revision.empty-file: 'Leere Datei'
|
||||
gist.revision.no-changes: 'Keine Änderungen'
|
||||
gist.revision.no-revisions: 'Keine Änderungen zum Anzeigen'
|
||||
|
||||
settings: 'Einstellungen'
|
||||
settings.email: 'Email'
|
||||
settings.email-help: 'Für Commits und Gravatar genutzt'
|
||||
settings.email-set: 'Email setzen'
|
||||
settings.link-accounts: 'Accounts verlinken'
|
||||
settings.link-github-account: 'GitHub-Account verlinken'
|
||||
settings.link-gitlab-account: 'GitLab-Account verlinken'
|
||||
settings.link-gitea-account: 'Gitea-Account verlinken'
|
||||
settings.unlink-github-account: 'Github-Account Verlinkung aufheben'
|
||||
settings.unlink-gitlab-account: 'GitLab-Account Verlinkung aufheben'
|
||||
settings.unlink-gitea-account: 'Gitea-Account Verlinkung aufheben'
|
||||
settings.delete-account: 'Account löschen'
|
||||
settings.delete-account-confirm: 'Bist du dir sicher, dass du den Account löschen willst?'
|
||||
settings.add-ssh-key: 'SSH-Schlüssel hinzufügen'
|
||||
settings.add-ssh-key-help: 'Wird nur zum Pullen/Pushen von Gists mit Git über SSH genutzt'
|
||||
settings.add-ssh-key-title: 'Titel'
|
||||
settings.add-ssh-key-content: 'Schlüssel'
|
||||
settings.delete-ssh-key: 'Löschen'
|
||||
settings.delete-ssh-key-confirm: 'Entfernen von SSH-Schlüssel bestätigen'
|
||||
settings.ssh-key-added-at: 'Hinzugefügt'
|
||||
settings.ssh-key-never-used: 'Nie benutzt'
|
||||
settings.ssh-key-last-used: 'Zuletzt benutzt'
|
||||
settings.change-username: 'Benutzername ändern'
|
||||
settings.create-password: 'Password erstellen'
|
||||
settings.create-password-help: 'Passwort erstellen'
|
||||
settings.change-password: 'Passwort ändern'
|
||||
settings.change-password-help: 'Passwort ändern'
|
||||
settings.password-label-title: 'Passwort'
|
||||
|
||||
auth.signup-disabled: 'Das Registrieren wurde vom Administrator deaktiviert'
|
||||
auth.login: 'Anmeldung'
|
||||
auth.signup: 'Registration'
|
||||
auth.new-account: 'Neuer Account'
|
||||
auth.username: 'Benutzername'
|
||||
auth.password: 'Passwort'
|
||||
auth.register-instead: 'Stattdessen registrieren'
|
||||
auth.login-instead: 'Stattdessen anmelden'
|
||||
|
||||
error: 'Fehler'
|
||||
|
||||
header.menu.all: 'Alle'
|
||||
header.menu.new: 'Neu'
|
||||
header.menu.search: 'Suchen'
|
||||
header.menu.my-gists: 'meine Gists'
|
||||
header.menu.liked: 'Favorisiert'
|
||||
header.menu.admin: 'Admin'
|
||||
header.menu.settings: 'Einstellungen'
|
||||
header.menu.logout: 'Abmelden'
|
||||
header.menu.register: 'Registrieren'
|
||||
header.menu.login: 'Anmelden'
|
||||
header.menu.light: 'Hell'
|
||||
header.menu.dark: 'Dunkel'
|
||||
header.menu.system: 'System'
|
||||
footer.powered-by: 'Powered by %s'
|
||||
|
||||
pagination.older: 'Älter'
|
||||
pagination.newer: 'Neuer'
|
||||
pagination.previous: 'Vorherige'
|
||||
pagination.next: 'Nachfolgende'
|
||||
|
||||
admin.admin_panel: 'Admin Panel'
|
||||
admin.general: 'Allgemein'
|
||||
admin.users: 'Benutzer'
|
||||
admin.gists: 'Gists'
|
||||
admin.configuration: 'Konfiguration'
|
||||
admin.versions: 'Versionen'
|
||||
admin.ssh_keys: 'SSH Schlüssel'
|
||||
admin.stats: 'Statistiken'
|
||||
admin.actions: 'Aktionen'
|
||||
admin.actions.sync-fs: 'Gists auf dem Dateisystem sychronisieren'
|
||||
admin.actions.sync-db: 'Gists von der Datenbank synchronisieren'
|
||||
admin.actions.git-gc: '„garbage collection“ bei allen git Repositories ausführen'
|
||||
admin.actions.sync-previews: 'Alle Gist Vorschauen synchronisieren'
|
||||
admin.actions.reset-hooks: 'Alle Git server Hooks für alle Repositories synchronisieren'
|
||||
admin.actions.index-gists: 'Alle Gists Indexieren'
|
||||
admin.id: 'ID'
|
||||
admin.user: 'Benutzer'
|
||||
admin.delete: 'Löschen'
|
||||
admin.created_at: 'Erstellt'
|
||||
|
||||
admin.config-link: 'Diese Konfiguration kann mithilfe der YAML Konfigurationsdatei und/oder Umgebungsvariablen %s werden.'
|
||||
admin.config-link-overriden: 'überschrieben'
|
||||
admin.disable-signup: 'Registrierung deaktivieren'
|
||||
admin.disable-signup_help: 'Die Erstellung neuer Accounts verbieten.'
|
||||
admin.require-login: 'Anmeldung nötig'
|
||||
admin.require-login_help: 'Benutzer müssen sich anmelden, bevor sie Gists ansehen können.'
|
||||
admin.disable-login: 'Login-Maske deaktivieren'
|
||||
admin.disable-login_help: 'Login über Login-Maske verbieten und Benutzung von OAuth Providern erzwingen.'
|
||||
admin.disable-gravatar: 'Gravatar deaktivieren'
|
||||
admin.disable-gravatar_help: 'Gravatar als Avatar-Anbieter deaktivieren.'
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: 'Willst du diesen Benutzer löschen?'
|
||||
|
||||
admin.gists.title: 'Titel'
|
||||
admin.gists.private: 'privat?'
|
||||
admin.gists.nb-files: 'Anz. Dateien'
|
||||
admin.gists.nb-likes: 'Anz. Favoriten'
|
||||
admin.gists.delete_confirm: 'Willst du diese Gist löschen?'
|
||||
auth.oauth: ''
|
||||
gist.new.preview: ''
|
||||
gist.new.create-a-new-gist: ''
|
||||
gist.edit.edit-gist: ''
|
||||
gist.list.all-liked-by: ''
|
||||
gist.list.all-forked-by: ''
|
||||
gist.list.all-from: ''
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-be-empty: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
269
internal/i18n/locales/en-US.yml
Normal file
269
internal/i18n/locales/en-US.yml
Normal file
@@ -0,0 +1,269 @@
|
||||
gist.public: Public
|
||||
gist.unlisted: Unlisted
|
||||
gist.private: Private
|
||||
|
||||
gist.header.like: Like
|
||||
gist.header.unlike: Unlike
|
||||
gist.header.fork: Fork
|
||||
gist.header.edit: Edit
|
||||
gist.header.delete: Delete
|
||||
gist.header.forked-from: Forked from
|
||||
gist.header.last-active: Last active
|
||||
gist.header.select-tab: Select a tab
|
||||
gist.header.code: Code
|
||||
gist.header.revisions: Revisions
|
||||
gist.header.revision: Revision
|
||||
gist.header.clone-http: Clone via %s
|
||||
gist.header.clone-http-help: Clone with Git using HTTP basic authentication.
|
||||
gist.header.clone-ssh: Clone via SSH
|
||||
gist.header.clone-ssh-help: Clone with Git using an SSH key.
|
||||
gist.header.embed: Embed
|
||||
gist.header.embed-help: Embed this gist to your website.
|
||||
gist.header.download-zip: Download ZIP
|
||||
|
||||
gist.raw: Raw
|
||||
gist.file-truncated: This file has been truncated.
|
||||
gist.watch-full-file: View the full file.
|
||||
gist.file-not-valid: This file is not a valid CSV file.
|
||||
gist.no-content: No files found
|
||||
|
||||
gist.new.new_gist: New gist
|
||||
gist.new.title: Title
|
||||
gist.new.description: Description
|
||||
gist.new.url: URL
|
||||
gist.new.filename-with-extension: Filename with extension
|
||||
gist.new.indent-mode: Indent mode
|
||||
gist.new.indent-mode-space: Space
|
||||
gist.new.indent-mode-tab: Tab
|
||||
gist.new.indent-size: Indent size
|
||||
gist.new.wrap-mode: Wrap mode
|
||||
gist.new.wrap-mode-no: No wrap
|
||||
gist.new.wrap-mode-soft: Soft wrap
|
||||
gist.new.add-file: Add file
|
||||
gist.new.create-public-button: Create public gist
|
||||
gist.new.create-unlisted-button: Create unlisted gist
|
||||
gist.new.create-private-button: Create private gist
|
||||
gist.new.preview: Preview
|
||||
gist.new.create-a-new-gist: Create a new gist
|
||||
|
||||
gist.edit.editing: Editing
|
||||
gist.edit.edit-gist: Edit %s
|
||||
gist.edit.change-visibility: Make
|
||||
gist.edit.delete: Delete
|
||||
gist.edit.cancel: Cancel
|
||||
gist.edit.save: Save
|
||||
|
||||
gist.list.joined: Joined
|
||||
gist.list.all: All gists
|
||||
gist.list.search-results: Search results
|
||||
gist.list.sort: Sort
|
||||
gist.list.sort-by-created: created
|
||||
gist.list.sort-by-updated: updated
|
||||
gist.list.order-by-asc: Least recently
|
||||
gist.list.order-by-desc: Recently
|
||||
gist.list.select-tab: Select a tab
|
||||
gist.list.liked: Liked
|
||||
gist.list.likes: likes
|
||||
gist.list.forked: Forked
|
||||
gist.list.forked-from: Forked from
|
||||
gist.list.forks: forks
|
||||
gist.list.files: files
|
||||
gist.list.last-active: Last active
|
||||
gist.list.no-gists: No gists
|
||||
gist.list.all-liked-by: All gists liked by %s
|
||||
gist.list.all-forked-by: All gists forked by %s
|
||||
gist.list.all-from: All gists from %s
|
||||
|
||||
gist.search.found: gists found
|
||||
gist.search.no-results: No gists found
|
||||
gist.search.help.user: gists created by user
|
||||
gist.search.help.title: gists with given title
|
||||
gist.search.help.filename: gists having files with given name
|
||||
gist.search.help.extension: gists having files with given extension
|
||||
gist.search.help.language: gists having files with given language
|
||||
|
||||
gist.forks: Forks
|
||||
gist.forks.view: View fork
|
||||
gist.forks.no: No public forks
|
||||
gist.forks.for: Forks for %s
|
||||
|
||||
gist.likes: Likes
|
||||
gist.likes.no: No likes yet
|
||||
gist.likes.for: Likes for %s
|
||||
|
||||
gist.revisions: Revisions
|
||||
gist.revision.revised: revised this gist
|
||||
gist.revision.go-to-revision: Go to revision
|
||||
gist.revision.file-created: file created
|
||||
gist.revision.file-deleted: file deleted
|
||||
gist.revision.file-renamed: renamed to
|
||||
gist.revision.diff-truncated: Diff is too large to be shown
|
||||
gist.revision.file-renamed-no-changes: File renamed without changes
|
||||
gist.revision.empty-file: Empty file
|
||||
gist.revision.no-changes: No changes
|
||||
gist.revision.no-revisions: No revisions to show
|
||||
gist.revision-of: Revision of %s
|
||||
|
||||
settings: Settings
|
||||
settings.email: Email
|
||||
settings.email-help: Used for commits and Gravatar
|
||||
settings.email-set: Set email
|
||||
settings.link-accounts: Link accounts
|
||||
settings.link-github-account: Link GitHub account
|
||||
settings.link-gitlab-account: Link GitLab account
|
||||
settings.link-gitea-account: Link Gitea account
|
||||
settings.unlink-github-account: Unlink GitHub account
|
||||
settings.unlink-gitlab-account: Unlink GitLab account
|
||||
settings.unlink-gitea-account: Unlink Gitea account
|
||||
settings.delete-account: Delete account
|
||||
settings.delete-account-confirm: Are you sure you want to delete your account ?
|
||||
settings.add-ssh-key: Add SSH key
|
||||
settings.add-ssh-key-help: Used only to pull/push gists using Git via SSH
|
||||
settings.add-ssh-key-title: Title
|
||||
settings.add-ssh-key-content: Key
|
||||
settings.delete-ssh-key: Delete
|
||||
settings.delete-ssh-key-confirm: Confirm deletion of SSH key
|
||||
settings.ssh-key-added-at: Added
|
||||
settings.ssh-key-never-used: Never used
|
||||
settings.ssh-key-last-used: Last used
|
||||
settings.ssh-key-exists: SSH key already exists
|
||||
settings.change-username: Change username
|
||||
settings.create-password: Create password
|
||||
settings.create-password-help: Create your password to login to Opengist via HTTP
|
||||
settings.change-password: Change password
|
||||
settings.change-password-help: Change your password to login to Opengist via HTTP
|
||||
settings.password-label-title: Password
|
||||
|
||||
auth.signup-disabled: Administrator has disabled signing up
|
||||
auth.login: Login
|
||||
auth.signup: Register
|
||||
auth.new-account: New account
|
||||
auth.username: Username
|
||||
auth.password: Password
|
||||
auth.register-instead: Register instead
|
||||
auth.login-instead: Login instead
|
||||
auth.oauth: Continue with %s account
|
||||
|
||||
error: Error
|
||||
error.page-not-found: Page not found
|
||||
error.bad-request: Bad request
|
||||
error.signup-disabled: Signing up is disabled
|
||||
error.signup-disabled-form: Signing up via registration form is disabled
|
||||
error.login-disabled-form: Logging in via login form is disabled
|
||||
error.complete-oauth-login: "Cannot complete user auth: %s"
|
||||
error.oauth-unsupported: Unsupported provider
|
||||
error.cannot-bind-data: Cannot bind data
|
||||
error.invalid-number: Invalid number
|
||||
error.invalid-character-unescaped: Invalid character unescaped
|
||||
|
||||
header.menu.all: All
|
||||
header.menu.new: New
|
||||
header.menu.search: Search
|
||||
header.menu.my-gists: My gists
|
||||
header.menu.liked: Liked
|
||||
header.menu.admin: Admin
|
||||
header.menu.settings: Settings
|
||||
header.menu.logout: Logout
|
||||
header.menu.register: Register
|
||||
header.menu.login: Login
|
||||
header.menu.light: Light
|
||||
header.menu.dark: Dark
|
||||
header.menu.system: System
|
||||
footer.powered-by: Powered by %s
|
||||
|
||||
pagination.older: Older
|
||||
pagination.newer: Newer
|
||||
pagination.previous: Previous
|
||||
pagination.next: Next
|
||||
|
||||
admin.admin_panel: Admin panel
|
||||
admin.general: General
|
||||
admin.users: Users
|
||||
admin.gists: Gists
|
||||
admin.configuration: Configuration
|
||||
admin.invitations: Invitations
|
||||
admin.invitations.create: Create invitation
|
||||
admin.versions: Versions
|
||||
admin.ssh_keys: SSH keys
|
||||
admin.stats: Stats
|
||||
admin.actions: Actions
|
||||
admin.actions.sync-fs: Synchronize gists from filesystem
|
||||
admin.actions.sync-db: Synchronize gists from database
|
||||
admin.actions.git-gc: Garbage collect all git repositories
|
||||
admin.actions.sync-previews: Synchronize all gists previews
|
||||
admin.actions.reset-hooks: Reset Git server hooks for all repositories
|
||||
admin.actions.index-gists: Index all gists
|
||||
admin.id: ID
|
||||
admin.user: User
|
||||
admin.delete: Delete
|
||||
admin.created_at: Created
|
||||
|
||||
admin.config-link: This configuration can be %s by a YAML config file and/or environment variables.
|
||||
admin.config-link-overriden: overridden
|
||||
admin.disable-signup: Disable signup
|
||||
admin.disable-signup_help: Forbid the creation of new accounts.
|
||||
admin.require-login: Require login
|
||||
admin.require-login_help: Enforce users to be logged in to see gists.
|
||||
admin.allow-gists-without-login: Allow individual gists without login
|
||||
admin.allow-gists-without-login_help: Allow individual gists to be viewed and downloaded without login, while requiring login for discovering gists.
|
||||
admin.disable-login: Disable login form
|
||||
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
|
||||
admin.disable-gravatar: Disable Gravatar
|
||||
admin.disable-gravatar_help: Disable the usage of Gravatar as an avatar provider.
|
||||
|
||||
admin.users.delete_confirm: Do you want to delete this user ?
|
||||
|
||||
admin.gists.title: Title
|
||||
admin.gists.private: Private ?
|
||||
admin.gists.nb-files: Nb. files
|
||||
admin.gists.nb-likes: Nb. likes
|
||||
admin.gists.delete_confirm: Do you want to delete this gist ?
|
||||
|
||||
admin.invitations.help: Invitations can be used to create an account even if signing up is disabled.
|
||||
admin.invitations.max_uses: Max uses
|
||||
admin.invitations.expires_at: Expires at
|
||||
admin.invitations.code: Code
|
||||
admin.invitations.copy_link: Copy link
|
||||
admin.invitations.uses: Uses
|
||||
admin.invitations.expired: Expired
|
||||
|
||||
flash.admin.user-deleted: User has been deleted
|
||||
flash.admin.gist-deleted: Gist has been deleted
|
||||
flash.admin.invitation-created: Invitation has been created
|
||||
flash.admin.invitation-deleted: Invitation has been deleted
|
||||
flash.admin.sync-fs: Syncing repositories from filesystem...
|
||||
flash.admin.sync-db: Syncing repositories from database...
|
||||
flash.admin.git-gc: Garbage collecting repositories...
|
||||
flash.admin.sync-previews: Syncing Gist previews...
|
||||
flash.admin.reset-hooks: Resetting Git server hooks for all repositories...
|
||||
flash.admin.index-gists: Indexing all gists...
|
||||
|
||||
flash.auth.username-exists: Username already exists
|
||||
flash.auth.invalid-credentials: Invalid credentials
|
||||
flash.auth.account-linked-oauth: Account linked to %s
|
||||
flash.auth.account-unlinked-oauth: Account unlinked from %s
|
||||
flash.auth.user-sshkeys-not-retrievable: Could not get user keys
|
||||
flash.auth.user-sshkeys-not-created: Could not create ssh key
|
||||
flash.auth.must-be-logged-in: You must be logged in to access gists
|
||||
|
||||
flash.gist.visibility-changed: Gist visibility has been changed
|
||||
flash.gist.deleted: Gist has been deleted
|
||||
flash.gist.fork-own-gist: Unable to fork own gists
|
||||
flash.gist.forked: Gist has been forked
|
||||
|
||||
flash.user.email-updated: Email updated
|
||||
flash.user.invalid-ssh-key: Invalid SSH key
|
||||
flash.user.ssh-key-added: SSH key added
|
||||
flash.user.ssh-key-deleted: SSH key deleted
|
||||
flash.user.password-updated: Password updated
|
||||
flash.user.username-updated: Username updated
|
||||
|
||||
validation.is-too-long: Field %s is too long
|
||||
validation.should-not-be-empty: Field %s should not be empty
|
||||
validation.should-not-include-sub-directory: Field %s should not include a sub directory
|
||||
validation.should-only-contain-alphanumeric-characters: Field %s should only contain alphanumeric characters
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes
|
||||
validation.not-enough: Not enough %s
|
||||
validation.invalid: Invalid %s
|
||||
|
||||
html.title.admin-panel: Admin panel
|
||||
259
internal/i18n/locales/es-ES.yml
Normal file
259
internal/i18n/locales/es-ES.yml
Normal file
@@ -0,0 +1,259 @@
|
||||
gist.public: Público
|
||||
gist.unlisted: No listado
|
||||
gist.private: Privado
|
||||
|
||||
gist.header.like: Me gusta
|
||||
gist.header.unlike: No me gusta
|
||||
gist.header.fork: Bifurcar
|
||||
gist.header.edit: Editar
|
||||
gist.header.delete: Eliminar
|
||||
gist.header.forked-from: Bifurcado desde
|
||||
gist.header.last-active: Última actividad
|
||||
gist.header.select-tab: Seleccionar pestaña
|
||||
gist.header.code: Código
|
||||
gist.header.revisions: Revisiones
|
||||
gist.header.revision: Revisión
|
||||
gist.header.clone-http: Clonar via %s
|
||||
gist.header.clone-http-help: Clonar con Git usando autenticación básica HTTP.
|
||||
gist.header.clone-ssh: Clonar via SSH
|
||||
gist.header.clone-ssh-help: Clonar con Git usando una clave SSH.
|
||||
gist.header.embed: ''
|
||||
gist.header.embed-help: ''
|
||||
gist.header.download-zip: Descargar ZIP
|
||||
|
||||
gist.raw: Sin formato
|
||||
gist.file-truncated: Este archivo ha sido truncado.
|
||||
gist.watch-full-file: Ver el archivo completo.
|
||||
gist.file-not-valid: Este archivo no es un archivo CSV válido.
|
||||
gist.no-content: Sin contenido
|
||||
|
||||
gist.new.new_gist: Nuevo gist
|
||||
gist.new.title: Título
|
||||
gist.new.description: Descripción
|
||||
gist.new.filename-with-extension: Nombre de archivo con extensión
|
||||
gist.new.indent-mode: Modo de sangrado
|
||||
gist.new.indent-mode-space: Espacio
|
||||
gist.new.indent-mode-tab: Tabulación
|
||||
gist.new.indent-size: Tamaño de sangrado
|
||||
gist.new.wrap-mode: Modo de ajuste
|
||||
gist.new.wrap-mode-no: Sin ajuste
|
||||
gist.new.wrap-mode-soft: Ajuste suave
|
||||
gist.new.add-file: Agregar archivo
|
||||
gist.new.create-public-button: Crear gist público
|
||||
gist.new.create-unlisted-button: Crear gist no listado
|
||||
gist.new.create-private-button: Crear gist privado
|
||||
|
||||
gist.edit.editing: Editando
|
||||
gist.edit.change-visibility: Hacer
|
||||
gist.edit.delete: Eliminar
|
||||
gist.edit.cancel: Cancelar
|
||||
gist.edit.save: Guardar
|
||||
|
||||
gist.list.joined: Unido
|
||||
gist.list.all: Todos los gists
|
||||
gist.list.search-results: Resultados de búsqueda
|
||||
gist.list.sort: Ordenar
|
||||
gist.list.sort-by-created: creado
|
||||
gist.list.sort-by-updated: actualizado
|
||||
gist.list.order-by-asc: Menos reciente
|
||||
gist.list.order-by-desc: Recientemente
|
||||
gist.list.select-tab: Seleccionar pestaña
|
||||
gist.list.liked: Gustado
|
||||
gist.list.likes: gustos
|
||||
gist.list.forked: Bifurcado
|
||||
gist.list.forked-from: Bifurcado desde
|
||||
gist.list.forks: bifurcaciones
|
||||
gist.list.files: archivos
|
||||
gist.list.last-active: Última actividad
|
||||
gist.list.no-gists: Sin gists
|
||||
|
||||
gist.forks: Bifurcaciones
|
||||
gist.forks.view: Ver bifurcación
|
||||
gist.forks.no: No hay bifurcaciones públicas
|
||||
|
||||
gist.likes: Gustos
|
||||
gist.likes.no: Aún no hay gustos
|
||||
|
||||
gist.revisions: Revisiones
|
||||
gist.revision.revised: revisó este gist
|
||||
gist.revision.go-to-revision: Ir a la revisión
|
||||
gist.revision.file-created: archivo creado
|
||||
gist.revision.file-deleted: archivo eliminado
|
||||
gist.revision.file-renamed: renombrado a
|
||||
gist.revision.diff-truncated: Diferencia truncada porque es demasiado grande para mostrarse.
|
||||
gist.revision.file-renamed-no-changes: Archivo renombrado sin cambios
|
||||
gist.revision.empty-file: Archivo vacío
|
||||
gist.revision.no-changes: Sin cambios
|
||||
gist.revision.no-revisions: No hay revisiones para mostrar
|
||||
|
||||
settings: Configuración
|
||||
settings.email: Correo electrónico
|
||||
settings.email-help: Usado para confirmaciones y Gravatar
|
||||
settings.email-set: Establecer correo electrónico
|
||||
settings.link-accounts: Enlazar cuentas
|
||||
settings.link-github-account: Enlazar cuenta de GitHub
|
||||
settings.link-gitea-account: Enlazar cuenta de Gitea
|
||||
settings.unlink-github-account: Desenlazar cuenta de GitHub
|
||||
settings.unlink-gitea-account: Desenlazar cuenta de Gitea
|
||||
settings.delete-account: Eliminar cuenta
|
||||
settings.delete-account-confirm: ¿Estás seguro de que quieres eliminar tu cuenta?
|
||||
settings.add-ssh-key: Agregar clave SSH
|
||||
settings.add-ssh-key-help: Usado solo para extraer/push gists usando Git a través de SSH
|
||||
settings.add-ssh-key-title: Título
|
||||
settings.add-ssh-key-content: Clave
|
||||
settings.delete-ssh-key: Eliminar
|
||||
settings.delete-ssh-key-confirm: Confirmar eliminación de clave SSH
|
||||
settings.ssh-key-added-at: Añadido
|
||||
settings.ssh-key-never-used: Nunca usado
|
||||
settings.ssh-key-last-used: Último uso
|
||||
|
||||
auth.signup-disabled: El administrador ha deshabilitado el registro
|
||||
auth.login: Iniciar sesión
|
||||
auth.signup: Registrarse
|
||||
auth.new-account: Nueva cuenta
|
||||
auth.username: Nombre de usuario
|
||||
auth.password: Contraseña
|
||||
auth.register-instead: Registrarse en su lugar
|
||||
auth.login-instead: Iniciar sesión en su lugar
|
||||
auth.oauth: Continuar con cuenta de %s
|
||||
|
||||
error: Error
|
||||
|
||||
header.menu.all: Todos
|
||||
header.menu.new: Nuevo
|
||||
header.menu.search: Buscar
|
||||
header.menu.my-gists: Mis gists
|
||||
header.menu.liked: Gustados
|
||||
header.menu.admin: Administrador
|
||||
header.menu.settings: Configuración
|
||||
header.menu.logout: Cerrar sesión
|
||||
header.menu.register: Registrarse
|
||||
header.menu.login: Iniciar sesión
|
||||
header.menu.light: Claro
|
||||
header.menu.dark: Oscuro
|
||||
header.menu.system: Sistema
|
||||
footer.powered-by: Desarrollado por %s
|
||||
|
||||
pagination.older: Anterior
|
||||
pagination.newer: Siguiente
|
||||
pagination.previous: Anterior
|
||||
pagination.next: Siguiente
|
||||
|
||||
admin.admin_panel: Panel de administración
|
||||
admin.general: General
|
||||
admin.users: Usuarios
|
||||
admin.gists: Gists
|
||||
admin.configuration: Configuración
|
||||
admin.versions: Versiones
|
||||
admin.ssh_keys: Claves SSH
|
||||
admin.stats: Estadísticas
|
||||
admin.actions: Acciones
|
||||
admin.actions.sync-fs: Sincronizar gists desde el sistema de archivos
|
||||
admin.actions.sync-db: Sincronizar gists desde la base de datos
|
||||
admin.actions.git-gc: Recolectar basura en los repositorios Git
|
||||
admin.id: ID
|
||||
admin.user: Usuario
|
||||
admin.delete: Eliminar
|
||||
admin.created_at: Creado
|
||||
|
||||
admin.config-link: Esta configuración puede ser %s por un archivo de configuración YAML y/o variables de entorno.
|
||||
admin.disable-signup: Deshabilitar registro
|
||||
admin.disable-signup_help: Prohibir la creación de nuevas cuentas.
|
||||
admin.require-login: Requerir inicio de sesión
|
||||
admin.require-login_help: Obligar a los usuarios a iniciar sesión para ver gists.
|
||||
admin.disable-login: Deshabilitar formulario de inicio de sesión
|
||||
admin.disable-login_help: Prohibir el inicio de sesión a través del formulario de inicio de sesión para forzar el uso de proveedores de OAuth en su lugar.
|
||||
admin.disable-gravatar: Deshabilitar Gravatar
|
||||
admin.disable-gravatar_help: Deshabilitar el uso de Gravatar como proveedor de avatar.
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: ¿Quieres eliminar a este usuario?
|
||||
|
||||
admin.gists.title: Título
|
||||
admin.gists.private: ¿Privado?
|
||||
admin.gists.nb-files: Núm. de archivos
|
||||
admin.gists.nb-likes: Núm. de gustos
|
||||
admin.gists.delete_confirm: ¿Quieres eliminar este gist?
|
||||
gist.new.url: ''
|
||||
gist.new.preview: ''
|
||||
gist.new.create-a-new-gist: ''
|
||||
gist.edit.edit-gist: ''
|
||||
gist.list.all-liked-by: ''
|
||||
gist.list.all-forked-by: ''
|
||||
gist.list.all-from: ''
|
||||
gist.search.found: ''
|
||||
gist.search.no-results: ''
|
||||
gist.search.help.user: ''
|
||||
gist.search.help.title: ''
|
||||
gist.search.help.filename: ''
|
||||
gist.search.help.extension: ''
|
||||
gist.search.help.language: ''
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
settings.link-gitlab-account: ''
|
||||
settings.unlink-gitlab-account: ''
|
||||
settings.change-username: ''
|
||||
settings.create-password: ''
|
||||
settings.create-password-help: ''
|
||||
settings.change-password: ''
|
||||
settings.change-password-help: ''
|
||||
settings.password-label-title: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.actions.sync-previews: ''
|
||||
admin.actions.reset-hooks: ''
|
||||
admin.actions.index-gists: ''
|
||||
admin.config-link-overriden: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-be-empty: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
259
internal/i18n/locales/fr-FR.yml
Normal file
259
internal/i18n/locales/fr-FR.yml
Normal file
@@ -0,0 +1,259 @@
|
||||
gist.public: Public
|
||||
gist.unlisted: Non répertorié
|
||||
gist.private: Privé
|
||||
|
||||
gist.header.like: J'aime
|
||||
gist.header.unlike: Je n'aime plus
|
||||
gist.header.fork: Fork
|
||||
gist.header.edit: Éditer
|
||||
gist.header.delete: Supprimer
|
||||
gist.header.forked-from: Forké de
|
||||
gist.header.last-active: Dernière activité
|
||||
gist.header.select-tab: Sélectionner un onglet
|
||||
gist.header.code: Code
|
||||
gist.header.revisions: Révisions
|
||||
gist.header.revision: Révision
|
||||
gist.header.clone-http: Cloner via %s
|
||||
gist.header.clone-http-help: Cloner avec Git en utilisant l'authentification HTTP basic.
|
||||
gist.header.clone-ssh: Cloner via SSH
|
||||
gist.header.clone-ssh-help: Cloner avec Git en utilisant une clé SSH.
|
||||
gist.header.embed: Intégrer
|
||||
gist.header.embed-help: Intégrer ce gist dans une page web.
|
||||
gist.header.download-zip: Télécharger en ZIP
|
||||
|
||||
gist.raw: Brut
|
||||
gist.file-truncated: Ce fichier a été tronqué.
|
||||
gist.watch-full-file: Voir le fichier complet.
|
||||
gist.file-not-valid: Ce fichier n'est pas un fichier CSV valide.
|
||||
gist.no-content: Aucun fichier
|
||||
|
||||
gist.new.new_gist: Nouveau gist
|
||||
gist.new.title: Titre
|
||||
gist.new.description: Description
|
||||
gist.new.filename-with-extension: Nom de fichier avec extension
|
||||
gist.new.indent-mode: Mode d'indentation
|
||||
gist.new.indent-mode-space: Espace
|
||||
gist.new.indent-mode-tab: Tabulation
|
||||
gist.new.indent-size: Taille d'indentation
|
||||
gist.new.wrap-mode: Mode d'enroulement
|
||||
gist.new.wrap-mode-no: Sans enroulement
|
||||
gist.new.wrap-mode-soft: Enroulement doux
|
||||
gist.new.add-file: Ajouter un fichier
|
||||
gist.new.create-public-button: Créer un gist public
|
||||
gist.new.create-unlisted-button: Créer un gist non repertorié
|
||||
gist.new.create-private-button: Créer un gist privé
|
||||
|
||||
gist.edit.editing: Édition de
|
||||
gist.edit.change-visibility: Rendre
|
||||
gist.edit.delete: Supprimer
|
||||
gist.edit.cancel: Annuler
|
||||
gist.edit.save: Sauvegarder
|
||||
|
||||
gist.list.joined: Inscrit
|
||||
gist.list.all: Tous les gists
|
||||
gist.list.search-results: Résultats de recherche
|
||||
gist.list.sort: Trier
|
||||
gist.list.sort-by-created: créé
|
||||
gist.list.sort-by-updated: mis à jour
|
||||
gist.list.order-by-asc: Le moins récemment
|
||||
gist.list.order-by-desc: Récemment
|
||||
gist.list.select-tab: Sélectionner un onglet
|
||||
gist.list.liked: Aimé
|
||||
gist.list.likes: j'aimes
|
||||
gist.list.forked: Forké
|
||||
gist.list.forked-from: Forké de
|
||||
gist.list.forks: forks
|
||||
gist.list.files: fichiers
|
||||
gist.list.last-active: Dernière activité
|
||||
gist.list.no-gists: Aucun gist
|
||||
|
||||
gist.forks: Forks
|
||||
gist.forks.view: Voir le fork
|
||||
gist.forks.no: Pas de forks publics
|
||||
|
||||
gist.likes: J'aime
|
||||
gist.likes.no: Aucun j'aime pour le moment
|
||||
|
||||
gist.revisions: Révisions
|
||||
gist.revision.revised: a révisé ce gist
|
||||
gist.revision.go-to-revision: Aller à la révision
|
||||
gist.revision.file-created: fichier créé
|
||||
gist.revision.file-deleted: fichier supprimé
|
||||
gist.revision.file-renamed: renommé en
|
||||
gist.revision.diff-truncated: Révision trop volumineuse pour être affichée
|
||||
gist.revision.file-renamed-no-changes: Fichier renommé sans modifications
|
||||
gist.revision.empty-file: Fichier vide
|
||||
gist.revision.no-changes: Aucun changement
|
||||
gist.revision.no-revisions: Aucune révision à afficher
|
||||
|
||||
settings: Paramètres
|
||||
settings.email: Email
|
||||
settings.email-help: Utilisé pour les commits et Gravatar
|
||||
settings.email-set: Définir l'email
|
||||
settings.link-accounts: Lier les comptes
|
||||
settings.link-github-account: Lier le compte GitHub
|
||||
settings.link-gitea-account: Lier le compte Gitea
|
||||
settings.unlink-github-account: Détacher le compte GitHub
|
||||
settings.unlink-gitea-account: Détacher le compte Gitea
|
||||
settings.delete-account: Supprimer le compte
|
||||
settings.delete-account-confirm: Êtes-vous sûr de vouloir supprimer votre compte ?
|
||||
settings.add-ssh-key: Ajouter une clé SSH
|
||||
settings.add-ssh-key-help: Utilisé uniquement pour pull/push des gists avec Git via SSH
|
||||
settings.add-ssh-key-title: Titre
|
||||
settings.add-ssh-key-content: Clé
|
||||
settings.delete-ssh-key: Supprimer
|
||||
settings.delete-ssh-key-confirm: Confirmer la suppression de la clé SSH
|
||||
settings.ssh-key-added-at: Ajouté
|
||||
settings.ssh-key-never-used: Jamais utilisé
|
||||
settings.ssh-key-last-used: Dernière utilisation
|
||||
|
||||
auth.signup-disabled: L'administrateur a désactivé l'inscription
|
||||
auth.login: Connexion
|
||||
auth.signup: Inscription
|
||||
auth.new-account: Nouveau compte
|
||||
auth.username: Nom d'utilisateur
|
||||
auth.password: Mot de passe
|
||||
auth.register-instead: Je préfère m'inscrire
|
||||
auth.login-instead: Je préfère me connecter
|
||||
auth.oauth: Continuer avec un compte %s
|
||||
|
||||
error: Erreur
|
||||
|
||||
header.menu.all: Tous
|
||||
header.menu.new: Nouveau
|
||||
header.menu.search: Recherche
|
||||
header.menu.my-gists: Mes gists
|
||||
header.menu.liked: Aimés
|
||||
header.menu.admin: Admin
|
||||
header.menu.settings: Paramètres
|
||||
header.menu.logout: Déconnexion
|
||||
header.menu.register: Inscription
|
||||
header.menu.login: Connexion
|
||||
header.menu.light: Clair
|
||||
header.menu.dark: Sombre
|
||||
header.menu.system: Système
|
||||
footer.powered-by: Propulsé par %s
|
||||
|
||||
pagination.older: Plus ancien
|
||||
pagination.newer: Plus récent
|
||||
pagination.previous: Précédent
|
||||
pagination.next: Suivant
|
||||
|
||||
admin.admin_panel: Panneau d'administration
|
||||
admin.general: Général
|
||||
admin.users: Utilisateurs
|
||||
admin.gists: Gists
|
||||
admin.configuration: Configuration
|
||||
admin.versions: Versions
|
||||
admin.ssh_keys: Clés SSH
|
||||
admin.stats: Statistiques
|
||||
admin.actions: Actions
|
||||
admin.actions.sync-fs: Synchroniser les gists depuis le système de fichiers
|
||||
admin.actions.sync-db: Synchroniser les gists depuis la base de données
|
||||
admin.actions.git-gc: Nettoyage des dépôts git
|
||||
admin.id: ID
|
||||
admin.user: Utilisateur
|
||||
admin.delete: Supprimer
|
||||
admin.created_at: Créé
|
||||
|
||||
admin.config-link: Cette configuration peut être %s par un fichier de configuration YAML et/ou des variables d'environnement.
|
||||
admin.config-link-overriden: remplacée
|
||||
admin.disable-signup: Désactiver l'inscription
|
||||
admin.disable-signup_help: Interdire la création de nouveaux comptes.
|
||||
admin.require-login: Exiger la connexion
|
||||
admin.require-login_help: Obliger les utilisateurs à être connectés pour voir les gists.
|
||||
admin.disable-login: Désactiver le formulaire de connexion
|
||||
admin.disable-login_help: Interdire la connexion via le formulaire de connexion pour forcer l'utilisation des fournisseurs OAuth à la place.
|
||||
admin.disable-gravatar: Désactiver Gravatar
|
||||
admin.disable-gravatar_help: Désactiver l'utilisation de Gravatar comme fournisseur d'avatar.
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: Voulez-vous supprimer cet utilisateur ?
|
||||
|
||||
admin.gists.title: Titre
|
||||
admin.gists.private: Privé ?
|
||||
admin.gists.nb-files: Nb. de fichiers
|
||||
admin.gists.nb-likes: Nb. de j'aime
|
||||
admin.gists.delete_confirm: Voulez-vous supprimer ce gist ?
|
||||
gist.search.help.user: gists créés par un utilisateur
|
||||
gist.search.help.title: gists avec un titre spécifique
|
||||
gist.search.help.extension: gists qui ont des fichiers avec une extension spécifique
|
||||
gist.search.found: gists trouvés
|
||||
gist.search.help.filename: gists qui ont des fichiers avec un nom spécifique
|
||||
settings.link-gitlab-account: Lier le compte GitLab
|
||||
gist.search.help.language: gists qui ont des fichiers écrits en un langage spécifique
|
||||
settings.change-username: Changer le nom d'utilisateur
|
||||
settings.create-password: Créer un mot de passe
|
||||
settings.create-password-help: Créer un mot de passe pour se connecter à Opengist via HTTP
|
||||
settings.change-password: Changer le mot de passe
|
||||
settings.change-password-help: Changer le mot de passe pour se connecter à Opengist via HTTP
|
||||
settings.password-label-title: Mot de passe
|
||||
admin.actions.sync-previews: Synchroniser l'aperçu des gists
|
||||
admin.actions.reset-hooks: Réinitialiser les hooks de Git pour tous les dépôts
|
||||
gist.new.url: URL
|
||||
gist.search.no-results: Aucun gist trouvé
|
||||
settings.unlink-gitlab-account: Détacher le compte GitLab
|
||||
admin.actions.index-gists: Indexer tous les gists
|
||||
gist.new.preview: ''
|
||||
gist.new.create-a-new-gist: ''
|
||||
gist.edit.edit-gist: ''
|
||||
gist.list.all-liked-by: ''
|
||||
gist.list.all-forked-by: ''
|
||||
gist.list.all-from: ''
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-be-empty: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
6
internal/i18n/locales/fs_embed.go
Normal file
6
internal/i18n/locales/fs_embed.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package locales
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.yml
|
||||
var Files embed.FS
|
||||
260
internal/i18n/locales/hu-HU.yml
Normal file
260
internal/i18n/locales/hu-HU.yml
Normal file
@@ -0,0 +1,260 @@
|
||||
gist.public: Nyilvános
|
||||
gist.unlisted: Nem listázott
|
||||
gist.private: Privát
|
||||
|
||||
gist.header.like: Tetszik
|
||||
gist.header.unlike: Nem tetszik
|
||||
gist.header.fork: Fork
|
||||
gist.header.edit: Szerkesztés
|
||||
gist.header.delete: Törlés
|
||||
gist.header.forked-from: "Forkolva innen:"
|
||||
gist.header.last-active: Utoljára aktív
|
||||
gist.header.select-tab: Fül választása
|
||||
gist.header.code: Kód
|
||||
gist.header.revisions: Revíziók
|
||||
gist.header.revision: Revízió
|
||||
gist.header.clone-http: "Clone-ozás ezzel: %s"
|
||||
gist.header.clone-http-help: Clone-ozás Git HTTP basic hitelesítéssel.
|
||||
gist.header.clone-ssh: Clone-ozás SSH-n keresztül
|
||||
gist.header.clone-ssh-help: Clone-ozás SSH kulccsal
|
||||
gist.header.embed: ''
|
||||
gist.header.embed-help: ''
|
||||
gist.header.download-zip: ZIP archívum letöltése
|
||||
|
||||
gist.raw: Eredeti
|
||||
gist.file-truncated: Ennek a fájlnak nem az egész tartalma lett megjelenítve.
|
||||
gist.watch-full-file: Tekintsd meg a fájl egész tartalmát.
|
||||
gist.file-not-valid: Ez nem egy érvényes CSV fájl.
|
||||
gist.no-content: Nincs tartalom
|
||||
|
||||
gist.new.new_gist: Új gist
|
||||
gist.new.title: Cím
|
||||
gist.new.description: Leírás
|
||||
gist.new.url: URL
|
||||
gist.new.filename-with-extension: Fájlnév kiterjesztéssel
|
||||
gist.new.indent-mode: Indentáció típusa
|
||||
gist.new.indent-mode-space: Szóköz
|
||||
gist.new.indent-mode-tab: Tabulátor
|
||||
gist.new.indent-size: Indentáció méret
|
||||
gist.new.wrap-mode: Sortörés típusa
|
||||
gist.new.wrap-mode-no: Nincs sortörés
|
||||
gist.new.wrap-mode-soft: Csak megjelenítéskor
|
||||
gist.new.add-file: Fájl hozzáadása
|
||||
gist.new.create-public-button: Nyilvános gist létrehozása
|
||||
gist.new.create-unlisted-button: Nem listázott gist létrehozása
|
||||
gist.new.create-private-button: Privát gist létrehozása
|
||||
|
||||
gist.edit.editing: Szerkesztés
|
||||
gist.edit.change-visibility: "Állítsd erre:"
|
||||
gist.edit.delete: Törlés
|
||||
gist.edit.cancel: Mégse
|
||||
gist.edit.save: Mentés
|
||||
|
||||
gist.list.joined: Hozzáadva
|
||||
gist.list.all: Összes gist
|
||||
gist.list.search-results: Keresési erdemények
|
||||
gist.list.sort: Rendezés
|
||||
gist.list.sort-by-created: létrehozva
|
||||
gist.list.sort-by-updated: módosítva
|
||||
gist.list.order-by-asc: Utolsótól
|
||||
gist.list.order-by-desc: Újabbtól
|
||||
gist.list.select-tab: Fül választása
|
||||
gist.list.liked: Tetszik
|
||||
gist.list.likes: Kedvelések
|
||||
gist.list.forked: Forkolva
|
||||
gist.list.forked-from: "Forkolva innen:"
|
||||
gist.list.forks: forkok
|
||||
gist.list.files: fájlok
|
||||
gist.list.last-active: Utoljára aktív
|
||||
gist.list.no-gists: Nincsenek gistek
|
||||
|
||||
gist.search.found: találat
|
||||
gist.search.no-results: Nincsenek találatok
|
||||
gist.search.help.user: létrehozva e felhasználó által
|
||||
gist.search.help.title: gistek egyező címmel
|
||||
gist.search.help.filename: gistek melyek tartalmaznak fájlt egyező névvel
|
||||
gist.search.help.extension: gistek melyek tartalmaznak fájlt egyező kiterjesztéssel
|
||||
gist.search.help.language: gistek melyek tartalmaznak fájlt egyező nyelvvel
|
||||
|
||||
gist.forks: Forkok
|
||||
gist.forks.view: Fork megtekintése
|
||||
gist.forks.no: Nincsenek nyilvános forkok
|
||||
|
||||
gist.likes: Kedvelések
|
||||
gist.likes.no: Még nincsenek kedvelések
|
||||
|
||||
gist.revisions: Revíziók
|
||||
gist.revision.revised: gist felülvizsgálása
|
||||
gist.revision.go-to-revision: Revízióhoz ugrás
|
||||
gist.revision.file-created: fájl létrehozva
|
||||
gist.revision.file-deleted: fájl törölve
|
||||
gist.revision.file-renamed: "fájl átnevezve erre: "
|
||||
gist.revision.diff-truncated: Nem a teljes Diff lett megjelítve, mert túl hosszú lenne
|
||||
gist.revision.file-renamed-no-changes: Fájl átnevezve változtatások nélkül
|
||||
gist.revision.empty-file: Üres fájl
|
||||
gist.revision.no-changes: Nincsenek változtatások
|
||||
gist.revision.no-revisions: Nincsenek megjeleníthető revíziók
|
||||
|
||||
settings: Beállítások
|
||||
settings.email: Email
|
||||
settings.email-help: Commitoknál és Gravatarnál van használva
|
||||
settings.email-set: Email beállítása
|
||||
settings.link-accounts: Fiókok összekötése
|
||||
settings.link-github-account: GitHub fiók hozzáadása
|
||||
settings.link-gitlab-account: GitLab fiók hozzáadása
|
||||
settings.link-gitea-account: Gitea fiók hozzáadása
|
||||
settings.unlink-github-account: GitHub fiók eltávolítása
|
||||
settings.unlink-gitlab-account: GitLab fiók eltávolítása
|
||||
settings.unlink-gitea-account: Gitea fiók eltávolítása
|
||||
settings.delete-account: Fiók törlése
|
||||
settings.delete-account-confirm: Biztosan törölni szeretnéd a fiókod?
|
||||
settings.add-ssh-key: SSH kulcs hozzáadása
|
||||
settings.add-ssh-key-help: Csak SSH-n kersztül történő pull/push műveleteknél van használva
|
||||
settings.add-ssh-key-title: Cím
|
||||
settings.add-ssh-key-content: Kulcs
|
||||
settings.delete-ssh-key: Törlés
|
||||
settings.delete-ssh-key-confirm: Erősítsd meg az SSH kulcs törlését
|
||||
settings.ssh-key-added-at: "Hozzáadva:"
|
||||
settings.ssh-key-never-used: Sosem használt
|
||||
settings.ssh-key-last-used: "Utoljára használva:"
|
||||
settings.change-username: Felhasználónév megváltoztatása
|
||||
settings.create-password: Jelszó létrehozása
|
||||
settings.create-password-help: Hozz létre egy jelszót, hogy bejelentkezhess az OpenGist-be HTTP-n keresztül
|
||||
settings.change-password: Jelszó megváltoztatása
|
||||
settings.change-password-help: Változtasd meg a jelszót, amivel bejelentkezel az OpenGist-be HTTP-n keresztül
|
||||
settings.password-label-title: Jelszó
|
||||
|
||||
auth.signup-disabled: Az adminisztrátor kikapcsolta a regisztrációkat
|
||||
auth.login: Bejelentkezés
|
||||
auth.signup: Regisztráció
|
||||
auth.new-account: Új fiók
|
||||
auth.username: Felhasználónév
|
||||
auth.password: Jelszó
|
||||
auth.register-instead: Vagy regisztrálj
|
||||
auth.login-instead: Vagy jelentkezz be
|
||||
auth.oauth: Folytatás %s fiókkal
|
||||
|
||||
error: Hiba
|
||||
|
||||
header.menu.all: Minden
|
||||
header.menu.new: Új
|
||||
header.menu.search: Keresés
|
||||
header.menu.my-gists: Gistjeim
|
||||
header.menu.liked: Kedvelt
|
||||
header.menu.admin: Admin
|
||||
header.menu.settings: Beállítások
|
||||
header.menu.logout: Kijelentkezés
|
||||
header.menu.register: Regisztráció
|
||||
header.menu.login: Bejelentkezés
|
||||
header.menu.light: Világos
|
||||
header.menu.dark: Sötét
|
||||
header.menu.system: Rendszer
|
||||
footer.powered-by: Ez az oldal %s alapú
|
||||
|
||||
pagination.older: Régebbi
|
||||
pagination.newer: Újabb
|
||||
pagination.previous: Előző
|
||||
pagination.next: Következő
|
||||
|
||||
admin.admin_panel: Admin felület
|
||||
admin.general: Általános
|
||||
admin.users: Felhasználók
|
||||
admin.gists: Gistek
|
||||
admin.configuration: Konfiguráció
|
||||
admin.versions: Verziók
|
||||
admin.ssh_keys: SSH kulcsok
|
||||
admin.stats: Statisztikák
|
||||
admin.actions: Műveletek
|
||||
admin.actions.sync-fs: Gistek szinkronizálása a fájlrendszerrel
|
||||
admin.actions.sync-db: Gistek szinkronizálása az adatbázissal
|
||||
admin.actions.git-gc: Használatlan git repository-k eltávolítása
|
||||
admin.actions.sync-previews: Gist előnézetek szinkronizálása
|
||||
admin.actions.reset-hooks: Git server hook-ok alaphelyzetbe állítása minden repository-nál
|
||||
admin.actions.index-gists: Gistek indexelése
|
||||
admin.id: Azonosító
|
||||
admin.user: Felhasználó
|
||||
admin.delete: Törlés
|
||||
admin.created_at: Létrehozva
|
||||
|
||||
admin.config-link: Ezt a konfigurációt %s a YAML alapú konfigurációs fájl és/vagy környezeti változók.
|
||||
admin.config-link-overriden: felülírhatja
|
||||
admin.disable-signup: Regisztrációk letiltása
|
||||
admin.disable-signup_help: Letiltja a regisztráció lehetőségét.
|
||||
admin.require-login: Bejelentkezés szükséges
|
||||
admin.require-login_help: Csak bejelentkezett felhasználók láthatják a gisteket.
|
||||
admin.disable-login: Bejelentkezés űrlap letiltása
|
||||
admin.disable-login_help: Letiltja a bejelentkezés űrlapon keresztüli bejelentkezéseket, OAuth szolgáltatókat ajánlva helyette.
|
||||
admin.disable-gravatar: Gravatar kikapcsolása
|
||||
admin.disable-gravatar_help: Tiltsd le a Gravatar-t mint profilkép szolgáltató.
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: Biztosan törlöd ezt a felhasználót?
|
||||
|
||||
admin.gists.title: Cím
|
||||
admin.gists.private: Privát ?
|
||||
admin.gists.nb-files: Fájlok száma
|
||||
admin.gists.nb-likes: Kedv. száma
|
||||
admin.gists.delete_confirm: Biztosan törlöd a gistet?
|
||||
gist.new.preview: ''
|
||||
gist.new.create-a-new-gist: ''
|
||||
gist.edit.edit-gist: ''
|
||||
gist.list.all-liked-by: ''
|
||||
gist.list.all-forked-by: ''
|
||||
gist.list.all-from: ''
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-be-empty: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
269
internal/i18n/locales/it_IT.yml
Normal file
269
internal/i18n/locales/it_IT.yml
Normal file
@@ -0,0 +1,269 @@
|
||||
gist.public: 'Pubblico'
|
||||
gist.unlisted: 'Non in lista'
|
||||
gist.private: 'Privato'
|
||||
|
||||
gist.header.like: 'Mi piace'
|
||||
gist.header.unlike: 'Non mi piace più'
|
||||
gist.header.fork: 'Forka'
|
||||
gist.header.edit: 'Modifica'
|
||||
gist.header.delete: 'Elimina'
|
||||
gist.header.forked-from: 'Forkato da'
|
||||
gist.header.last-active: 'Ultima attività'
|
||||
gist.header.select-tab: 'Seleziona una scheda'
|
||||
gist.header.code: 'Codice'
|
||||
gist.header.revisions: 'Revisioni'
|
||||
gist.header.revision: 'Revisione'
|
||||
gist.header.clone-http: 'Clona tramite %s'
|
||||
gist.header.clone-http-help: 'Clona con Git usando l''autenticazione HTTP basic.'
|
||||
gist.header.clone-ssh: 'Clona tramite SSH'
|
||||
gist.header.clone-ssh-help: 'Clona con Git usando una chiave SSH.'
|
||||
gist.header.embed: 'Incorpora'
|
||||
gist.header.embed-help: 'Incorpora questo gist nel tuo sito.'
|
||||
gist.header.download-zip: 'Scarica ZIP'
|
||||
|
||||
gist.raw: 'Raw'
|
||||
gist.file-truncated: 'Questo file è stato troncato.'
|
||||
gist.watch-full-file: 'Visualizza il file completo.'
|
||||
gist.file-not-valid: 'Questo file non è un CSV valido.'
|
||||
gist.no-content: 'Nessun file trovato'
|
||||
|
||||
gist.new.new_gist: 'Nuovo gist'
|
||||
gist.new.title: 'Titolo'
|
||||
gist.new.description: 'Descrizione'
|
||||
gist.new.url: 'URL'
|
||||
gist.new.filename-with-extension: 'Nome del file con l''estensione'
|
||||
gist.new.indent-mode: 'Modalità indentazione'
|
||||
gist.new.indent-mode-space: 'Spazio'
|
||||
gist.new.indent-mode-tab: 'Tabulazione'
|
||||
gist.new.indent-size: 'Dimensione d''indentazione'
|
||||
gist.new.wrap-mode: 'Modalità a capo'
|
||||
gist.new.wrap-mode-no: 'Non andare a capo'
|
||||
gist.new.wrap-mode-soft: 'Vai a capo dove necessario'
|
||||
gist.new.add-file: 'Aggiungi un file'
|
||||
gist.new.create-public-button: 'Crea un gist pubblico'
|
||||
gist.new.create-unlisted-button: 'Crea un gist non in lista'
|
||||
gist.new.create-private-button: 'Crea un gist privato'
|
||||
gist.new.preview: 'Anteprima'
|
||||
gist.new.create-a-new-gist: 'Crea un nuovo gist'
|
||||
|
||||
gist.edit.editing: 'Modificando'
|
||||
gist.edit.edit-gist: 'Modifica %s'
|
||||
gist.edit.change-visibility: 'Crea'
|
||||
gist.edit.delete: 'Elimina'
|
||||
gist.edit.cancel: 'Annulla'
|
||||
gist.edit.save: 'Salva'
|
||||
|
||||
gist.list.joined: 'Unito'
|
||||
gist.list.all: 'Tutti i gists'
|
||||
gist.list.search-results: 'Risultati della ricerca'
|
||||
gist.list.sort: 'Ordina'
|
||||
gist.list.sort-by-created: 'creazione'
|
||||
gist.list.sort-by-updated: 'aggiornamento'
|
||||
gist.list.order-by-asc: 'Meno recente'
|
||||
gist.list.order-by-desc: 'Più recente'
|
||||
gist.list.select-tab: 'Seleziona una scheda'
|
||||
gist.list.liked: 'Gists che mi piacciono'
|
||||
gist.list.likes: 'mi piace'
|
||||
gist.list.forked: 'Forkati'
|
||||
gist.list.forked-from: 'Forkato da'
|
||||
gist.list.forks: 'forks'
|
||||
gist.list.files: 'files'
|
||||
gist.list.last-active: 'Ultima volta attivo'
|
||||
gist.list.no-gists: 'Nessun gist'
|
||||
gist.list.all-liked-by: 'Tutti i gists che piacciono a %s'
|
||||
gist.list.all-forked-by: 'Tutti i gists forkati da %s'
|
||||
gist.list.all-from: 'Tutti i gists di %s'
|
||||
|
||||
gist.search.found: 'gists trovati'
|
||||
gist.search.no-results: 'Nessun gist trovato'
|
||||
gist.search.help.user: 'utente che ha creato il gist'
|
||||
gist.search.help.title: 'titolo del gist'
|
||||
gist.search.help.filename: 'nome di file nel gist'
|
||||
gist.search.help.extension: 'estensione del file nel gist'
|
||||
gist.search.help.language: 'linguaggio del file nel gist'
|
||||
|
||||
gist.forks: 'Forks'
|
||||
gist.forks.view: 'Visualizza fork'
|
||||
gist.forks.no: 'Nessun fork pubblico'
|
||||
gist.forks.for: 'Forks di %s'
|
||||
|
||||
gist.likes: 'Mi piace'
|
||||
gist.likes.no: 'Ancora nessun mi piace'
|
||||
gist.likes.for: 'Mi piace per %s'
|
||||
|
||||
gist.revisions: 'Revisioni'
|
||||
gist.revision.revised: 'ha revisionato questo gist'
|
||||
gist.revision.go-to-revision: 'Vai alla revisione'
|
||||
gist.revision.file-created: 'file creato'
|
||||
gist.revision.file-deleted: 'file eliminato'
|
||||
gist.revision.file-renamed: 'rinominato come'
|
||||
gist.revision.diff-truncated: 'Il diff è troppo grande per essere visualizzato'
|
||||
gist.revision.file-renamed-no-changes: 'File rinominato senza modifiche'
|
||||
gist.revision.empty-file: 'File vuoto'
|
||||
gist.revision.no-changes: 'Nessuna modifica'
|
||||
gist.revision.no-revisions: 'Nessuna revisione da mostrare'
|
||||
gist.revision-of: 'Revisioni di %s'
|
||||
|
||||
settings: 'Impostazioni'
|
||||
settings.email: 'Email'
|
||||
settings.email-help: 'Usato per i commits e per Gravatar'
|
||||
settings.email-set: 'Imposta email'
|
||||
settings.link-accounts: 'Collega accounts'
|
||||
settings.link-github-account: 'Collega account di GitHub'
|
||||
settings.link-gitlab-account: 'Collega account di GitLab'
|
||||
settings.link-gitea-account: 'Collega account di Gitea'
|
||||
settings.unlink-github-account: 'Scollega account di GitHub'
|
||||
settings.unlink-gitlab-account: 'Scollega account di GitLab'
|
||||
settings.unlink-gitea-account: 'Scollega account di Gitea'
|
||||
settings.delete-account: 'Elimina account'
|
||||
settings.delete-account-confirm: 'Sei sicuro di voler eliminare il tuo account?'
|
||||
settings.add-ssh-key: 'Aggiungi chiave SSH'
|
||||
settings.add-ssh-key-help: 'Utilizzata soltanto per pullare/pushare gists con Git tramite SSH'
|
||||
settings.add-ssh-key-title: 'Titolo'
|
||||
settings.add-ssh-key-content: 'Chiave'
|
||||
settings.delete-ssh-key: 'Elimina'
|
||||
settings.delete-ssh-key-confirm: 'Conferma eliminazione della chiave SSH'
|
||||
settings.ssh-key-added-at: 'Aggiunta'
|
||||
settings.ssh-key-never-used: 'Mai usata'
|
||||
settings.ssh-key-last-used: 'Usata l''ultima volta'
|
||||
settings.change-username: 'Cambia nome utente'
|
||||
settings.create-password: 'Crea password'
|
||||
settings.create-password-help: 'Crea la tua password per entrare in Opengist tramite HTTP'
|
||||
settings.change-password: 'Cambia password'
|
||||
settings.change-password-help: 'Cambia la tua password per entrare in Opengist tramite HTTP'
|
||||
settings.password-label-title: 'Password'
|
||||
|
||||
auth.signup-disabled: 'L''amministratore ha disabilitato la registrazione'
|
||||
auth.login: 'Entra'
|
||||
auth.signup: 'Registrati'
|
||||
auth.new-account: 'Nuovo account'
|
||||
auth.username: 'Nome utente'
|
||||
auth.password: 'Password'
|
||||
auth.register-instead: 'Non hai ancora un account?'
|
||||
auth.login-instead: 'Hai già un account?'
|
||||
auth.oauth: 'Continua con l''account %s'
|
||||
|
||||
error: 'Errore'
|
||||
error.page-not-found: 'Pagina non trovata'
|
||||
error.bad-request: 'Richiesta errata'
|
||||
error.signup-disabled: 'La registrazione è disabilitata'
|
||||
error.signup-disabled-form: 'La registrazione tramtie form è disabilitata'
|
||||
error.login-disabled-form: 'Il login tramite form è disabilitato'
|
||||
error.complete-oauth-login: "Impossibile completare l'autenticazione di %s"
|
||||
error.oauth-unsupported: 'Provider non supportato'
|
||||
error.cannot-bind-data: 'Impossibile abbinare i dati'
|
||||
error.invalid-number: 'Numero non valido'
|
||||
error.invalid-character-unescaped: 'Caratteri non escapati non validi'
|
||||
|
||||
header.menu.all: 'Tutti'
|
||||
header.menu.new: 'Nuovi'
|
||||
header.menu.search: 'Cerca'
|
||||
header.menu.my-gists: 'I miei gists'
|
||||
header.menu.liked: 'Gists che mi piacciono'
|
||||
header.menu.admin: 'Amministrazione'
|
||||
header.menu.settings: 'Impostazioni'
|
||||
header.menu.logout: 'Esci'
|
||||
header.menu.register: 'Registrati'
|
||||
header.menu.login: 'Entra'
|
||||
header.menu.light: 'Chiaro'
|
||||
header.menu.dark: 'Scuro'
|
||||
header.menu.system: 'Sistema'
|
||||
footer.powered-by: 'Creato da %s'
|
||||
|
||||
pagination.older: 'Più vecchi'
|
||||
pagination.newer: 'Più nuovi'
|
||||
pagination.previous: 'Precedente'
|
||||
pagination.next: 'Successiva'
|
||||
|
||||
admin.admin_panel: 'Pannello amministrazione'
|
||||
admin.general: 'Generale'
|
||||
admin.users: 'Utenti'
|
||||
admin.gists: 'Gists'
|
||||
admin.configuration: 'Configurazione'
|
||||
admin.invitations: 'Inviti'
|
||||
admin.invitations.create: 'Crea un invito'
|
||||
admin.versions: 'Versioni'
|
||||
admin.ssh_keys: 'Chiavi SSH'
|
||||
admin.stats: 'Statistiche'
|
||||
admin.actions: 'Azioni'
|
||||
admin.actions.sync-fs: 'Sincronizza gists dal filesystem'
|
||||
admin.actions.sync-db: 'Sincronizza gists dal database'
|
||||
admin.actions.git-gc: 'Esegui la garbage collection da tutti i repositories'
|
||||
admin.actions.sync-previews: 'Sincronizza tutte le anteprime dei gists'
|
||||
admin.actions.reset-hooks: 'Resetta tutti gli hook del server Git per tutti i repositories'
|
||||
admin.actions.index-gists: 'Indicizza tutti i gists'
|
||||
admin.id: 'ID'
|
||||
admin.user: 'Utente'
|
||||
admin.delete: 'Elimina'
|
||||
admin.created_at: 'Creato'
|
||||
|
||||
admin.config-link: 'Questa configurazione può essere %s da un file di configurazione YAML o da delle variabili d''ambiente.'
|
||||
admin.config-link-overriden: 'sovrascritta'
|
||||
admin.disable-signup: 'Disabilita la registrazione'
|
||||
admin.disable-signup_help: 'Blocca la creazione di nuovi accounts.'
|
||||
admin.require-login: 'Richiedi login'
|
||||
admin.require-login_help: 'Obbliga gli utenti ad essere loggati per vedere i gists.'
|
||||
admin.allow-gists-without-login: 'Permetti di creare gists individuali senza login'
|
||||
admin.allow-gists-without-login_help: 'Permetti di visualizzare e scaricare gists individuali senza essere loggati, ma richiedi il login per scoprire nuovi gists.'
|
||||
admin.disable-login: 'Disabilita form di login'
|
||||
admin.disable-login_help: 'Blocca il login tramite form per forzare l''accesso tramite Oauth.'
|
||||
admin.disable-gravatar: 'Disabilita Gravatar'
|
||||
admin.disable-gravatar_help: 'Disabilita Gravatar come provider di avatar.'
|
||||
|
||||
admin.users.delete_confirm: 'Vuoi eliminare questo utente?'
|
||||
|
||||
admin.gists.title: 'Titolo'
|
||||
admin.gists.private: 'Privato?'
|
||||
admin.gists.nb-files: 'N. files'
|
||||
admin.gists.nb-likes: 'N. mi piace'
|
||||
admin.gists.delete_confirm: 'Vuoi eliminare questo gist?'
|
||||
|
||||
admin.invitations.help: 'Gli inviti possono essere usati per creare un account anche se la registazione è disabilitata.'
|
||||
admin.invitations.max_uses: 'Utenti massimi'
|
||||
admin.invitations.expires_at: 'Scade il'
|
||||
admin.invitations.code: 'Codice'
|
||||
admin.invitations.copy_link: 'Copia link'
|
||||
admin.invitations.uses: 'Usa'
|
||||
admin.invitations.expired: 'Scaduto'
|
||||
|
||||
flash.admin.user-deleted: 'L''utente è stato eliminato'
|
||||
flash.admin.gist-deleted: 'Il gist è stato eliminato'
|
||||
flash.admin.invitation-created: 'L''invito è stato creato'
|
||||
flash.admin.invitation-deleted: 'L''invito è stato eliminato'
|
||||
flash.admin.sync-fs: 'Sincronizzando i repositories dal filesystem...'
|
||||
flash.admin.sync-db: 'Sincronizzando i repositories dal database...'
|
||||
flash.admin.git-gc: 'Eseguendo il garbage collector dei repositories...'
|
||||
flash.admin.sync-previews: 'Sincronizzando le anteprime dei gists...'
|
||||
flash.admin.reset-hooks: 'Resettando gli hook di Git per tutti i repositories...'
|
||||
flash.admin.index-gists: 'Indicizzando tutti i gists...'
|
||||
|
||||
flash.auth.username-exists: 'Il nome utente esiste già'
|
||||
flash.auth.invalid-credentials: 'Credenziali errate'
|
||||
flash.auth.account-linked-oauth: 'Account collegato a %s'
|
||||
flash.auth.account-unlinked-oauth: 'Account scollegato da %s'
|
||||
flash.auth.user-sshkeys-not-retrievable: 'Impossibile ottenere le chiavi dell''utente'
|
||||
flash.auth.user-sshkeys-not-created: 'Impossibile creare chiave SSH'
|
||||
flash.auth.must-be-logged-in: 'Devi essere loggato per visualizzare questi gists'
|
||||
|
||||
flash.gist.visibility-changed: 'La visibilità del gist è stata modificata'
|
||||
flash.gist.deleted: 'Il gist è stato eliminato'
|
||||
flash.gist.fork-own-gist: 'Impossibile forkare i propri gists'
|
||||
flash.gist.forked: 'Il gist è stato forkato'
|
||||
|
||||
flash.user.email-updated: 'Email aggiornata'
|
||||
flash.user.invalid-ssh-key: 'Chiave SSH non valida'
|
||||
flash.user.ssh-key-added: 'Chiave SSH aggiunta'
|
||||
flash.user.ssh-key-deleted: 'Chiave SSH eliminata'
|
||||
flash.user.password-updated: 'Password aggiornata'
|
||||
flash.user.username-updated: 'Nome utente aggiornato'
|
||||
|
||||
validation.is-too-long: 'Il campo %s è troppo lungo'
|
||||
validation.should-not-be-empty: 'Il campo %s non può essere vuoto'
|
||||
validation.should-not-include-sub-directory: 'Il campo %s non può contenere una sottocartella'
|
||||
validation.should-only-contain-alphanumeric-characters: 'Il campo %s deve contenere solo caratteri alfanumerici'
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: 'Il campo %s può contenere solo caratteri alfanumerici e trattini'
|
||||
validation.not-enough: 'Non abbastanza %s'
|
||||
validation.invalid: '%s non valido'
|
||||
|
||||
html.title.admin-panel: 'Pannello amministratore'
|
||||
settings.ssh-key-exists: Questa chiave SSH esiste già
|
||||
259
internal/i18n/locales/pt-BR.yml
Normal file
259
internal/i18n/locales/pt-BR.yml
Normal file
@@ -0,0 +1,259 @@
|
||||
gist.public: Público
|
||||
gist.unlisted: Não listado
|
||||
gist.private: Privado
|
||||
|
||||
gist.header.like: Curtir
|
||||
gist.header.unlike: Não curtir
|
||||
gist.header.fork: Bifurcar
|
||||
gist.header.edit: Editar
|
||||
gist.header.delete: Excluir
|
||||
gist.header.forked-from: Bifurcado de
|
||||
gist.header.last-active: Última atividade
|
||||
gist.header.select-tab: Selecionar aba
|
||||
gist.header.code: Código
|
||||
gist.header.revisions: Revisões
|
||||
gist.header.revision: Revisão
|
||||
gist.header.clone-http: Clonar via %s
|
||||
gist.header.clone-http-help: Clonar com Git usando autenticação básica HTTP.
|
||||
gist.header.clone-ssh: Clonar via SSH
|
||||
gist.header.clone-ssh-help: Clonar com Git usando uma chave SSH.
|
||||
gist.header.download-zip: Baixar ZIP
|
||||
|
||||
gist.raw: Bruto
|
||||
gist.file-truncated: Este arquivo foi truncado.
|
||||
gist.watch-full-file: Ver arquivo completo.
|
||||
gist.file-not-valid: Este arquivo não é um arquivo CSV válido.
|
||||
gist.no-content: Sem conteúdo
|
||||
|
||||
gist.new.new_gist: Novo gist
|
||||
gist.new.title: Título
|
||||
gist.new.description: Descrição
|
||||
gist.new.filename-with-extension: Nome do arquivo com extensão
|
||||
gist.new.indent-mode: Modo de indentação
|
||||
gist.new.indent-mode-space: Espaço
|
||||
gist.new.indent-mode-tab: Tabulação
|
||||
gist.new.indent-size: Tamanho da indentação
|
||||
gist.new.wrap-mode: Modo de quebra
|
||||
gist.new.wrap-mode-no: Sem quebra
|
||||
gist.new.wrap-mode-soft: Quebra suave
|
||||
gist.new.add-file: Adicionar arquivo
|
||||
gist.new.create-public-button: Criar gist público
|
||||
gist.new.create-unlisted-button: Criar gist não listado
|
||||
gist.new.create-private-button: Criar gist privado
|
||||
|
||||
gist.edit.editing: Editando
|
||||
gist.edit.change-visibility: Alterar visibilidade
|
||||
gist.edit.delete: Excluir
|
||||
gist.edit.cancel: Cancelar
|
||||
gist.edit.save: Salvar
|
||||
|
||||
gist.list.joined: Juntou-se
|
||||
gist.list.all: Todos os gists
|
||||
gist.list.search-results: Resultados da busca
|
||||
gist.list.sort: Ordenar
|
||||
gist.list.sort-by-created: criado
|
||||
gist.list.sort-by-updated: atualizado
|
||||
gist.list.order-by-asc: Menos recente
|
||||
gist.list.order-by-desc: Mais recente
|
||||
gist.list.select-tab: Selecionar aba
|
||||
gist.list.liked: Curtido
|
||||
gist.list.likes: curtidas
|
||||
gist.list.forked: Bifurcado
|
||||
gist.list.forked-from: Bifurcado de
|
||||
gist.list.forks: bifurcações
|
||||
gist.list.files: arquivos
|
||||
gist.list.last-active: Última atividade
|
||||
gist.list.no-gists: Sem gists
|
||||
|
||||
gist.forks: Bifurcações
|
||||
gist.forks.view: Ver bifurcação
|
||||
gist.forks.no: Não há bifurcações públicas
|
||||
|
||||
gist.likes: Curtidas
|
||||
gist.likes.no: Ainda não há curtidas
|
||||
|
||||
gist.revisions: Revisões
|
||||
gist.revision.revised: revisou este gist
|
||||
gist.revision.go-to-revision: Ir para a revisão
|
||||
gist.revision.file-created: arquivo criado
|
||||
gist.revision.file-deleted: arquivo excluído
|
||||
gist.revision.file-renamed: renomeado para
|
||||
gist.revision.diff-truncated: Diferença truncada porque é muito grande para ser exibida.
|
||||
gist.revision.file-renamed-no-changes: Arquivo renomeado sem alterações
|
||||
gist.revision.empty-file: Arquivo vazio
|
||||
gist.revision.no-changes: Sem alterações
|
||||
gist.revision.no-revisions: Não há revisões para mostrar
|
||||
|
||||
settings: Configurações
|
||||
settings.email: E-mail
|
||||
settings.email-help: Usado para confirmações e Gravatar
|
||||
settings.email-set: Configurar e-mail
|
||||
settings.link-accounts: Vincular contas
|
||||
settings.link-github-account: Vincular conta do GitHub
|
||||
settings.link-gitea-account: Vincular conta do Gitea
|
||||
settings.unlink-github-account: Desvincular conta do GitHub
|
||||
settings.unlink-gitea-account: Desvincular conta do Gitea
|
||||
settings.delete-account: Excluir conta
|
||||
settings.delete-account-confirm: Tem certeza de que deseja excluir sua conta?
|
||||
settings.add-ssh-key: Adicionar chave SSH
|
||||
settings.add-ssh-key-help: Usado apenas para extrair/puxar gists usando Git via SSH
|
||||
settings.add-ssh-key-title: Título
|
||||
settings.add-ssh-key-content: Chave
|
||||
settings.delete-ssh-key: Excluir
|
||||
settings.delete-ssh-key-confirm: Confirmar exclusão da chave SSH
|
||||
settings.ssh-key-added-at: Adicionado
|
||||
settings.ssh-key-never-used: Nunca usado
|
||||
settings.ssh-key-last-used: Último uso
|
||||
|
||||
auth.signup-disabled: O administrador desabilitou o registro
|
||||
auth.login: Entrar
|
||||
auth.signup: Cadastrar-se
|
||||
auth.new-account: Nova conta
|
||||
auth.username: Nome de usuário
|
||||
auth.password: Senha
|
||||
auth.register-instead: Registrar-se no lugar
|
||||
auth.login-instead: Entrar no lugar
|
||||
auth.oauth: Continuar com conta do %s
|
||||
|
||||
error: Erro
|
||||
|
||||
header.menu.all: Todos
|
||||
header.menu.new: Novo
|
||||
header.menu.search: Buscar
|
||||
header.menu.my-gists: Meus gists
|
||||
header.menu.liked: Curtidos
|
||||
header.menu.admin: Administrador
|
||||
header.menu.settings: Configurações
|
||||
header.menu.logout: Sair
|
||||
header.menu.register: Registrar-se
|
||||
header.menu.login: Entrar
|
||||
header.menu.light: Claro
|
||||
header.menu.dark: Escuro
|
||||
header.menu.system: Sistema
|
||||
footer.powered-by: Desenvolvido por %s
|
||||
|
||||
pagination.older: Anterior
|
||||
pagination.newer: Próximo
|
||||
pagination.previous: Anterior
|
||||
pagination.next: Próximo
|
||||
|
||||
admin.admin_panel: Painel de administração
|
||||
admin.general: Geral
|
||||
admin.users: Usuários
|
||||
admin.gists: Gists
|
||||
admin.configuration: Configuração
|
||||
admin.versions: Versões
|
||||
admin.ssh_keys: Chaves SSH
|
||||
admin.stats: Estatísticas
|
||||
admin.actions: Ações
|
||||
admin.actions.sync-fs: Sincronizar gists do sistema de arquivos
|
||||
admin.actions.sync-db: Sincronizar gists do banco de dados
|
||||
admin.actions.git-gc: Coletar lixo nos repositórios Git
|
||||
admin.id: ID
|
||||
admin.user: Usuário
|
||||
admin.delete: Excluir
|
||||
admin.created_at: Criado
|
||||
|
||||
admin.config-link: Esta configuração pode ser %s por um arquivo de configuração YAML e/ou variáveis de ambiente.
|
||||
admin.disable-signup: Desabilitar registro
|
||||
admin.disable-signup_help: Proibir a criação de novas contas.
|
||||
admin.require-login: Exigir login
|
||||
admin.require-login_help: Obrigar os usuários a fazerem login para ver gists.
|
||||
admin.disable-login: Desabilitar formulário de login
|
||||
admin.disable-login_help: Proibir o login através do formulário de login para forçar o uso de provedores de OAuth no lugar.
|
||||
admin.disable-gravatar: Desabilitar Gravatar
|
||||
admin.disable-gravatar_help: Desabilitar o uso do Gravatar como provedor de avatar.
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: Quer excluir este usuário?
|
||||
|
||||
admin.gists.title: Título
|
||||
admin.gists.private: Privado
|
||||
admin.gists.nb-files: Núm. de arquivos
|
||||
admin.gists.nb-likes: Núm. de curtidas
|
||||
admin.gists.delete_confirm: Quer excluir este gist?
|
||||
flash.admin.index-gists: ''
|
||||
gist.header.embed: ''
|
||||
gist.header.embed-help: ''
|
||||
gist.new.url: ''
|
||||
gist.list.all-liked-by: ''
|
||||
gist.new.preview: ''
|
||||
gist.new.create-a-new-gist: ''
|
||||
gist.edit.edit-gist: ''
|
||||
gist.list.all-forked-by: ''
|
||||
gist.list.all-from: ''
|
||||
gist.search.found: ''
|
||||
gist.search.no-results: ''
|
||||
gist.search.help.user: ''
|
||||
gist.search.help.title: ''
|
||||
gist.search.help.filename: ''
|
||||
gist.search.help.extension: ''
|
||||
gist.search.help.language: ''
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
settings.link-gitlab-account: ''
|
||||
settings.unlink-gitlab-account: ''
|
||||
settings.change-username: ''
|
||||
settings.create-password: ''
|
||||
settings.create-password-help: ''
|
||||
settings.change-password: ''
|
||||
settings.change-password-help: ''
|
||||
settings.password-label-title: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.actions.sync-previews: ''
|
||||
admin.actions.reset-hooks: ''
|
||||
admin.actions.index-gists: ''
|
||||
admin.config-link-overriden: ''
|
||||
validation.invalid: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-be-empty: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
html.title.admin-panel: ''
|
||||
259
internal/i18n/locales/ru-RU.yml
Normal file
259
internal/i18n/locales/ru-RU.yml
Normal file
@@ -0,0 +1,259 @@
|
||||
gist.public: Публичный
|
||||
gist.unlisted: Скрытый
|
||||
gist.private: Приватный
|
||||
|
||||
gist.header.like: Нравится
|
||||
gist.header.unlike: Не нравится
|
||||
gist.header.fork: Создать форк
|
||||
gist.header.edit: Редактировать
|
||||
gist.header.delete: Удалить
|
||||
gist.header.forked-from: Форк с
|
||||
gist.header.last-active: Последняя активность
|
||||
gist.header.select-tab: Перейти
|
||||
gist.header.code: Код
|
||||
gist.header.revisions: Версии
|
||||
gist.header.revision: Версия
|
||||
gist.header.clone-http: Клонировать с помощью %s
|
||||
gist.header.clone-http-help: Клонировать с помощью Git используя аутентификацию HTTP.
|
||||
gist.header.clone-ssh: Клонировать c помощью SSH
|
||||
gist.header.clone-ssh-help: Клонировать c помощью Git используя ключ SSH.
|
||||
gist.header.embed: ''
|
||||
gist.header.embed-help: ''
|
||||
gist.header.download-zip: Скачать ZIP-архив
|
||||
|
||||
gist.raw: Исходник
|
||||
gist.file-truncated: Файл был обрезан.
|
||||
gist.watch-full-file: Просмотр всего файла.
|
||||
gist.file-not-valid: Невалидный CSV.
|
||||
gist.no-content: Нет данных
|
||||
|
||||
gist.new.new_gist: Новый фрагмент
|
||||
gist.new.title: Название
|
||||
gist.new.description: Описание
|
||||
gist.new.filename-with-extension: Имя файла с расширением
|
||||
gist.new.indent-mode: Отступы
|
||||
gist.new.indent-mode-space: Пробелы
|
||||
gist.new.indent-mode-tab: Табуляция
|
||||
gist.new.indent-size: Размер отступа
|
||||
gist.new.wrap-mode: Переносы строк
|
||||
gist.new.wrap-mode-no: Без переносов
|
||||
gist.new.wrap-mode-soft: Мягкие переносы
|
||||
gist.new.add-file: Добавить файл
|
||||
gist.new.create-public-button: Создать публичный фрагмент
|
||||
gist.new.create-unlisted-button: Создать скрытый фрагмент
|
||||
gist.new.create-private-button: Создать приватный фрагмент
|
||||
|
||||
gist.edit.editing: Редактирование
|
||||
gist.edit.change-visibility: Применить
|
||||
gist.edit.delete: Удалить
|
||||
gist.edit.cancel: Отмена
|
||||
gist.edit.save: Сохранить
|
||||
|
||||
gist.list.joined: Зарегистрирован
|
||||
gist.list.all: Все фрагменты
|
||||
gist.list.search-results: Результаты поиска
|
||||
gist.list.sort: Сортировка
|
||||
gist.list.sort-by-created: по дате создания
|
||||
gist.list.sort-by-updated: по дате обновления
|
||||
gist.list.order-by-asc: Свежие снизу
|
||||
gist.list.order-by-desc: Свежие сверху
|
||||
gist.list.select-tab: Перейти
|
||||
gist.list.liked: Понравившиеся
|
||||
gist.list.likes: лайк(-ов)
|
||||
gist.list.forked: Форки
|
||||
gist.list.forked-from: Форки с
|
||||
gist.list.forks: форк(-ов)
|
||||
gist.list.files: файл(-ов)
|
||||
gist.list.last-active: Последняя активность
|
||||
gist.list.no-gists: Нет фрагментов
|
||||
|
||||
gist.forks: Форки
|
||||
gist.forks.view: Посмотреть форк
|
||||
gist.forks.no: Нет форков
|
||||
|
||||
gist.likes: Нравятся
|
||||
gist.likes.no: Нет
|
||||
|
||||
gist.revisions: Ревизии
|
||||
gist.revision.revised: ревизий этого фрагмента
|
||||
gist.revision.go-to-revision: К ревизии
|
||||
gist.revision.file-created: файл создан
|
||||
gist.revision.file-deleted: файл удалён
|
||||
gist.revision.file-renamed: переименован в
|
||||
gist.revision.diff-truncated: Разница (diff) обрезана, так как результат слишком большой для показа
|
||||
gist.revision.file-renamed-no-changes: Файл переименован без изменений
|
||||
gist.revision.empty-file: Пустой файл
|
||||
gist.revision.no-changes: Без изменений
|
||||
gist.revision.no-revisions: Нет ревизий
|
||||
|
||||
settings: Настройки
|
||||
settings.email: Адрес эл. почты
|
||||
settings.email-help: Нужен для коммитов и Gravatar
|
||||
settings.email-set: Сохранить адрес
|
||||
settings.link-accounts: Привязка доступов
|
||||
settings.link-github-account: Привязать доступ GitHub
|
||||
settings.link-gitea-account: Привязать доступ Gitea
|
||||
settings.unlink-github-account: Отвязать доступ GitHub
|
||||
settings.unlink-gitea-account: Отвязать доступ Gitea
|
||||
settings.delete-account: Удалить аккаунт
|
||||
settings.delete-account-confirm: Вы уверены что хотите удалить свой аккаунт?
|
||||
settings.add-ssh-key: Добавить ключ SSH
|
||||
settings.add-ssh-key-help: Нужен только для работы с фрагментами через Git+SSH
|
||||
settings.add-ssh-key-title: Название
|
||||
settings.add-ssh-key-content: Ключ
|
||||
settings.delete-ssh-key: Удалить
|
||||
settings.delete-ssh-key-confirm: Подтвердите удаления ключа SSH
|
||||
settings.ssh-key-added-at: Дата добавления
|
||||
settings.ssh-key-never-used: Не был использован
|
||||
settings.ssh-key-last-used: Последнее использование
|
||||
|
||||
auth.signup-disabled: Регистрация запрещена Администратором сервиса
|
||||
auth.login: Вход
|
||||
auth.signup: Регистрация
|
||||
auth.new-account: Новый аккаунт
|
||||
auth.username: Имя пользователя
|
||||
auth.password: Пароль
|
||||
auth.register-instead: Зарегистрироваться
|
||||
auth.login-instead: Войти
|
||||
auth.oauth: Войти с помощью доступа %s
|
||||
|
||||
error: Ошибка
|
||||
|
||||
header.menu.all: Все
|
||||
header.menu.new: Новый
|
||||
header.menu.search: Поиск
|
||||
header.menu.my-gists: Мои фрагменты
|
||||
header.menu.liked: Понравившиеся
|
||||
header.menu.admin: Администрирование
|
||||
header.menu.settings: Настройки
|
||||
header.menu.logout: Выйти
|
||||
header.menu.register: Регистрация
|
||||
header.menu.login: Войти
|
||||
header.menu.light: Светлая
|
||||
header.menu.dark: Тёмная
|
||||
header.menu.system: Системная
|
||||
footer.powered-by: Работает на %s
|
||||
|
||||
pagination.older: Позже
|
||||
pagination.newer: Новее
|
||||
pagination.previous: Предыдущий
|
||||
pagination.next: Следующий
|
||||
|
||||
admin.admin_panel: Панель управления
|
||||
admin.general: Общее
|
||||
admin.users: Пользователи
|
||||
admin.gists: Фрагменты
|
||||
admin.configuration: Настройки
|
||||
admin.versions: Версии
|
||||
admin.ssh_keys: Ключи SSH
|
||||
admin.stats: Статистика
|
||||
admin.actions: Действия
|
||||
admin.actions.sync-fs: Синхронизировать фрагменты из файловой системы
|
||||
admin.actions.sync-db: Синхронизировать фрагменты с базой данных
|
||||
admin.actions.git-gc: Сборка мусора в репозиториях Git
|
||||
admin.id: ID
|
||||
admin.user: Пользователь
|
||||
admin.delete: Удалить
|
||||
admin.created_at: Создан
|
||||
|
||||
admin.config-link: Эти настройки могут быть %s файлом конфигурации YAML и/или переменными окружения.
|
||||
admin.config-link-overriden: перекрыты
|
||||
admin.disable-signup: Запретить регистрацию
|
||||
admin.disable-signup_help: Запретить создание новых доступов
|
||||
admin.require-login: Требовать авторизацию
|
||||
admin.require-login_help: Запретить просмотр фрагментов без авторизации.
|
||||
admin.disable-login: Запретить авторизацию по паролю
|
||||
admin.disable-login_help: Запретить авторизацию с вводом пароля, форсировать внешнюю авторизацию через Gitea/GitHub.
|
||||
admin.disable-gravatar: Запретить Gravatar
|
||||
admin.disable-gravatar_help: Запретить использование Gravatar как провайдера изображений профиля.
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
|
||||
|
||||
admin.gists.title: Название
|
||||
admin.gists.private: Приватный
|
||||
admin.gists.nb-files: Файлов
|
||||
admin.gists.nb-likes: Понравилось
|
||||
admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент?
|
||||
gist.new.url: ''
|
||||
gist.new.preview: ''
|
||||
gist.new.create-a-new-gist: ''
|
||||
gist.edit.edit-gist: ''
|
||||
gist.list.all-liked-by: ''
|
||||
gist.list.all-forked-by: ''
|
||||
gist.list.all-from: ''
|
||||
gist.search.found: ''
|
||||
gist.search.no-results: ''
|
||||
gist.search.help.user: ''
|
||||
gist.search.help.title: ''
|
||||
gist.search.help.filename: ''
|
||||
gist.search.help.extension: ''
|
||||
gist.search.help.language: ''
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
settings.link-gitlab-account: ''
|
||||
settings.unlink-gitlab-account: ''
|
||||
settings.change-username: ''
|
||||
settings.create-password: ''
|
||||
settings.create-password-help: ''
|
||||
settings.change-password: ''
|
||||
settings.change-password-help: ''
|
||||
settings.password-label-title: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.actions.sync-previews: ''
|
||||
admin.actions.reset-hooks: ''
|
||||
admin.actions.index-gists: ''
|
||||
validation.should-not-be-empty: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
267
internal/i18n/locales/tr-TR.yml
Normal file
267
internal/i18n/locales/tr-TR.yml
Normal file
@@ -0,0 +1,267 @@
|
||||
gist.public: Herkese Açık
|
||||
gist.unlisted: Liste Dışı
|
||||
gist.private: Gizli
|
||||
|
||||
gist.header.like: Beğen
|
||||
gist.header.unlike: Beğenmekten Vazgeç
|
||||
gist.header.fork: Çatalla
|
||||
gist.header.edit: Düzenle
|
||||
gist.header.delete: Sil
|
||||
gist.header.forked-from: Çatallı
|
||||
gist.header.last-active: Son aktif
|
||||
gist.header.select-tab: Bir sekme seç
|
||||
gist.header.code: Kod
|
||||
gist.header.revisions: Revizyonlar
|
||||
gist.header.revision: Revizyon
|
||||
gist.header.clone-http: \%s aracılığıyla klonla
|
||||
gist.header.clone-http-help: HTTP temel kimlik doğrulamasını kullanarak Git ile klonlayın.
|
||||
gist.header.clone-ssh: SSH aracılığıyla klonla
|
||||
gist.header.clone-ssh-help: Bir SSH anahtarı kullanarak Git ile klonlayın.
|
||||
gist.header.embed: Yerleştirme
|
||||
gist.header.embed-help: Bu gisti web sitenize yerleştirin.
|
||||
gist.header.download-zip: ZIP'i indirin
|
||||
|
||||
gist.raw: Ham
|
||||
gist.file-truncated: Bu dosya kısaltılmıştır.
|
||||
gist.watch-full-file: Dosyanın tamamını görüntüleyin.
|
||||
gist.file-not-valid: Bu dosya geçerli bir CSV dosyası değildir.
|
||||
gist.no-content: Dosya bulunamadı
|
||||
|
||||
gist.new.new_gist: Yeni gist
|
||||
gist.new.title: Başlık
|
||||
gist.new.description: Description
|
||||
gist.new.url: URL
|
||||
gist.new.filename-with-extension: Uzantılı dosya adı
|
||||
gist.new.indent-mode: Girinti modu
|
||||
gist.new.indent-mode-space: Boşluk
|
||||
gist.new.indent-mode-tab: Tab
|
||||
gist.new.indent-size: Girinti boyutu
|
||||
gist.new.wrap-mode: ''
|
||||
gist.new.wrap-mode-no: ''
|
||||
gist.new.wrap-mode-soft: ''
|
||||
gist.new.add-file: Add file
|
||||
gist.new.create-public-button: Herkese açık gist oluştur
|
||||
gist.new.create-unlisted-button: Liste dışı gist oluştur
|
||||
gist.new.create-private-button: Gizli gist oluştur
|
||||
gist.new.preview: Ön izle
|
||||
gist.new.create-a-new-gist: Yeni bir gist oluştur
|
||||
|
||||
gist.edit.editing: Düzenleme
|
||||
gist.edit.edit-gist: '%s düzenle'
|
||||
gist.edit.change-visibility: ''
|
||||
gist.edit.delete: Delete
|
||||
gist.edit.cancel: İptal Et
|
||||
gist.edit.save: Kaydet
|
||||
|
||||
gist.list.joined: Katıldı
|
||||
gist.list.all: Tüm gistler
|
||||
gist.list.search-results: Arama sonuçları
|
||||
gist.list.sort: Sırala
|
||||
gist.list.sort-by-created: oluşturuldu
|
||||
gist.list.sort-by-updated: düzenlendi
|
||||
gist.list.order-by-asc: En son yakın zamanda
|
||||
gist.list.order-by-desc: Son zamanlarda
|
||||
gist.list.select-tab: Bir sekme seçin
|
||||
gist.list.liked: Beğenildi
|
||||
gist.list.likes: beğeniler
|
||||
gist.list.forked: Çatallı
|
||||
gist.list.forked-from: çatallandı
|
||||
gist.list.forks: çatallar
|
||||
gist.list.files: files
|
||||
gist.list.last-active: Son aktif
|
||||
gist.list.no-gists: Gistler yok
|
||||
gist.list.all-liked-by: '%s tarafından beğenilen tüm gistler'
|
||||
gist.list.all-forked-by: '%s tarafından beğenilen tüm çatallar'
|
||||
gist.list.all-from: '%s tüm gistleri'
|
||||
|
||||
gist.search.found: bulunan gistler
|
||||
gist.search.no-results: Hiç gist bulunamadı
|
||||
gist.search.help.user: gists created by user
|
||||
gist.search.help.title: gists with given title
|
||||
gist.search.help.filename: gists having files with given name
|
||||
gist.search.help.extension: gists having files with given extension
|
||||
gist.search.help.language: gists having files with given language
|
||||
|
||||
gist.forks: Forks
|
||||
gist.forks.view: View fork
|
||||
gist.forks.no: No public forks
|
||||
gist.forks.for: Forks for %s
|
||||
|
||||
gist.likes: Likes
|
||||
gist.likes.no: No likes yet
|
||||
gist.likes.for: Likes for %s
|
||||
|
||||
gist.revisions: Revisions
|
||||
gist.revision.revised: revised this gist
|
||||
gist.revision.go-to-revision: Go to revision
|
||||
gist.revision.file-created: file created
|
||||
gist.revision.file-deleted: file deleted
|
||||
gist.revision.file-renamed: renamed to
|
||||
gist.revision.diff-truncated: Diff is too large to be shown
|
||||
gist.revision.file-renamed-no-changes: File renamed without changes
|
||||
gist.revision.empty-file: Empty file
|
||||
gist.revision.no-changes: No changes
|
||||
gist.revision.no-revisions: No revisions to show
|
||||
gist.revision-of: Revision of %s
|
||||
|
||||
settings: Settings
|
||||
settings.email: Email
|
||||
settings.email-help: Used for commits and Gravatar
|
||||
settings.email-set: Set email
|
||||
settings.link-accounts: Link accounts
|
||||
settings.link-github-account: Link GitHub account
|
||||
settings.link-gitlab-account: Link GitLab account
|
||||
settings.link-gitea-account: Link Gitea account
|
||||
settings.unlink-github-account: Unlink GitHub account
|
||||
settings.unlink-gitlab-account: Unlink GitLab account
|
||||
settings.unlink-gitea-account: Unlink Gitea account
|
||||
settings.delete-account: Delete account
|
||||
settings.delete-account-confirm: Are you sure you want to delete your account ?
|
||||
settings.add-ssh-key: Add SSH key
|
||||
settings.add-ssh-key-help: Used only to pull/push gists using Git via SSH
|
||||
settings.add-ssh-key-title: Title
|
||||
settings.add-ssh-key-content: Key
|
||||
settings.delete-ssh-key: Delete
|
||||
settings.delete-ssh-key-confirm: Confirm deletion of SSH key
|
||||
settings.ssh-key-added-at: Added
|
||||
settings.ssh-key-never-used: Never used
|
||||
settings.ssh-key-last-used: Last used
|
||||
settings.change-username: Change username
|
||||
settings.create-password: Create password
|
||||
settings.create-password-help: Create your password to login to Opengist via HTTP
|
||||
settings.change-password: Change password
|
||||
settings.change-password-help: Change your password to login to Opengist via HTTP
|
||||
settings.password-label-title: Password
|
||||
|
||||
auth.signup-disabled: Administrator has disabled signing up
|
||||
auth.login: Login
|
||||
auth.signup: Register
|
||||
auth.new-account: New account
|
||||
auth.username: Username
|
||||
auth.password: Password
|
||||
auth.register-instead: Register instead
|
||||
auth.login-instead: Login instead
|
||||
auth.oauth: Continue with %s account
|
||||
|
||||
error: Error
|
||||
error.page-not-found: Page not found
|
||||
error.bad-request: Bad request
|
||||
error.signup-disabled: Signing up is disabled
|
||||
error.signup-disabled-form: Signing up via registration form is disabled
|
||||
error.login-disabled-form: Logging in via login form is disabled
|
||||
error.complete-oauth-login: "Cannot complete user auth: %s"
|
||||
error.oauth-unsupported: Unsupported provider
|
||||
error.cannot-bind-data: Cannot bind data
|
||||
error.invalid-number: Invalid number
|
||||
error.invalid-character-unescaped: Invalid character unescaped
|
||||
|
||||
header.menu.all: All
|
||||
header.menu.new: New
|
||||
header.menu.search: Search
|
||||
header.menu.my-gists: My gists
|
||||
header.menu.liked: Liked
|
||||
header.menu.admin: Admin
|
||||
header.menu.settings: Settings
|
||||
header.menu.logout: Logout
|
||||
header.menu.register: Register
|
||||
header.menu.login: Login
|
||||
header.menu.light: Light
|
||||
header.menu.dark: Dark
|
||||
header.menu.system: System
|
||||
footer.powered-by: Powered by %s
|
||||
|
||||
pagination.older: Older
|
||||
pagination.newer: Newer
|
||||
pagination.previous: Previous
|
||||
pagination.next: Next
|
||||
|
||||
admin.admin_panel: Admin panel
|
||||
admin.general: General
|
||||
admin.users: Users
|
||||
admin.gists: Gists
|
||||
admin.configuration: Configuration
|
||||
admin.invitations: Invitations
|
||||
admin.invitations.create: Create invitation
|
||||
admin.versions: Versions
|
||||
admin.ssh_keys: SSH keys
|
||||
admin.stats: Stats
|
||||
admin.actions: Actions
|
||||
admin.actions.sync-fs: Synchronize gists from filesystem
|
||||
admin.actions.sync-db: Synchronize gists from database
|
||||
admin.actions.git-gc: Garbage collect all git repositories
|
||||
admin.actions.sync-previews: Synchronize all gists previews
|
||||
admin.actions.reset-hooks: Reset Git server hooks for all repositories
|
||||
admin.actions.index-gists: Index all gists
|
||||
admin.id: ID
|
||||
admin.user: User
|
||||
admin.delete: Delete
|
||||
admin.created_at: Created
|
||||
|
||||
admin.config-link: This configuration can be %s by a YAML config file and/or environment variables.
|
||||
admin.config-link-overriden: overridden
|
||||
admin.disable-signup: Disable signup
|
||||
admin.disable-signup_help: Forbid the creation of new accounts.
|
||||
admin.require-login: Require login
|
||||
admin.require-login_help: Enforce users to be logged in to see gists.
|
||||
admin.disable-login: Disable login form
|
||||
admin.disable-login_help: Forbid logging in via the login form to force using OAuth providers instead.
|
||||
admin.disable-gravatar: Disable Gravatar
|
||||
admin.disable-gravatar_help: Disable the usage of Gravatar as an avatar provider.
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: Do you want to delete this user ?
|
||||
|
||||
admin.gists.title: Title
|
||||
admin.gists.private: Private ?
|
||||
admin.gists.nb-files: Nb. files
|
||||
admin.gists.nb-likes: Nb. likes
|
||||
admin.gists.delete_confirm: Do you want to delete this gist ?
|
||||
|
||||
admin.invitations.help: Invitations can be used to create an account even if signing up is disabled.
|
||||
admin.invitations.max_uses: Max uses
|
||||
admin.invitations.expires_at: Expires at
|
||||
admin.invitations.code: Code
|
||||
admin.invitations.copy_link: Copy link
|
||||
admin.invitations.uses: Uses
|
||||
admin.invitations.expired: Expired
|
||||
|
||||
flash.admin.user-deleted: User has been deleted
|
||||
flash.admin.gist-deleted: Gist has been deleted
|
||||
flash.admin.invitation-created: Invitation has been created
|
||||
flash.admin.invitation-deleted: Invitation has been deleted
|
||||
flash.admin.sync-fs: Syncing repositories from filesystem...
|
||||
flash.admin.sync-db: Syncing repositories from database...
|
||||
flash.admin.git-gc: Garbage collecting repositories...
|
||||
flash.admin.sync-previews: Syncing Gist previews...
|
||||
flash.admin.reset-hooks: Resetting Git server hooks for all repositories...
|
||||
flash.admin.index-gists: Indexing all gists...
|
||||
|
||||
flash.auth.username-exists: Username already exists
|
||||
flash.auth.invalid-credentials: Invalid credentials
|
||||
flash.auth.account-linked-oauth: Account linked to %s
|
||||
flash.auth.account-unlinked-oauth: Account unlinked from %s
|
||||
flash.auth.user-sshkeys-not-retrievable: Could not get user keys
|
||||
flash.auth.user-sshkeys-not-created: Could not create ssh key
|
||||
flash.auth.must-be-logged-in: You must be logged in to access gists
|
||||
|
||||
flash.gist.visibility-changed: Gist visibility has been changed
|
||||
flash.gist.deleted: Gist has been deleted
|
||||
flash.gist.fork-own-gist: Unable to fork own gists
|
||||
flash.gist.forked: Gist has been forked
|
||||
|
||||
flash.user.email-updated: Email updated
|
||||
flash.user.invalid-ssh-key: Invalid SSH key
|
||||
flash.user.ssh-key-added: SSH key added
|
||||
flash.user.ssh-key-deleted: SSH key deleted
|
||||
flash.user.password-updated: Password updated
|
||||
flash.user.username-updated: Username updated
|
||||
|
||||
validation.is-too-long: Field %s is too long
|
||||
validation.should-not-be-empty: Field %s should not be empty
|
||||
validation.should-not-include-sub-directory: Field %s should not include a sub directory
|
||||
validation.should-only-contain-alphanumeric-characters: Field %s should only contain alphanumeric characters
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes
|
||||
validation.not-enough: Not enough %s
|
||||
validation.invalid: Invalid %s
|
||||
|
||||
html.title.admin-panel: Admin panel
|
||||
259
internal/i18n/locales/zh-CN.yml
Normal file
259
internal/i18n/locales/zh-CN.yml
Normal file
@@ -0,0 +1,259 @@
|
||||
gist.public: 公开
|
||||
gist.unlisted: 非列出
|
||||
gist.private: 私有
|
||||
|
||||
gist.header.like: 喜欢
|
||||
gist.header.unlike: 取消喜欢
|
||||
gist.header.fork: 派生
|
||||
gist.header.edit: 编辑
|
||||
gist.header.delete: 删除
|
||||
gist.header.forked-from: 派生自
|
||||
gist.header.last-active: 最后活跃于
|
||||
gist.header.select-tab: Select a tab
|
||||
gist.header.code: 代码
|
||||
gist.header.revisions: 修订
|
||||
gist.header.revision: 修订
|
||||
gist.header.clone-http: 通过 %s 克隆
|
||||
gist.header.clone-http-help: 使用 Git 通过 HTTP 基础认证克隆。
|
||||
gist.header.clone-ssh: 通过 SSH 克隆
|
||||
gist.header.clone-ssh-help: 使用 Git 通过 SSH 密钥克隆。
|
||||
gist.header.embed: ''
|
||||
gist.header.embed-help: '在你的网页中嵌入此gist。'
|
||||
gist.header.download-zip: 下载 ZIP
|
||||
|
||||
gist.raw: 原始文件
|
||||
gist.file-truncated: 此文件已被截断。
|
||||
gist.watch-full-file: 查看完整文件。
|
||||
gist.file-not-valid: 此文件不是有效的 CSV 文件。
|
||||
gist.no-content: 没有内容
|
||||
|
||||
gist.new.new_gist: 创建 Gist
|
||||
gist.new.title: 标题
|
||||
gist.new.description: 描述
|
||||
gist.new.filename-with-extension: 文件名与扩展名
|
||||
gist.new.indent-mode: 缩进模式
|
||||
gist.new.indent-mode-space: 空格
|
||||
gist.new.indent-mode-tab: 制表符
|
||||
gist.new.indent-size: 缩进大小
|
||||
gist.new.wrap-mode: 换行模式
|
||||
gist.new.wrap-mode-no: 不自动换行
|
||||
gist.new.wrap-mode-soft: 软换行
|
||||
gist.new.add-file: 添加文件
|
||||
gist.new.create-public-button: 创建公开 Gist
|
||||
gist.new.create-unlisted-button: 创建非列出 Gist
|
||||
gist.new.create-private-button: 创建私有 Gist
|
||||
|
||||
gist.edit.editing: 编辑
|
||||
gist.edit.change-visibility: 设为
|
||||
gist.edit.delete: 删除
|
||||
gist.edit.cancel: 取消
|
||||
gist.edit.save: 保存
|
||||
|
||||
gist.list.joined: Joined
|
||||
gist.list.all: 所有 Gists
|
||||
gist.list.search-results: 搜索结果
|
||||
gist.list.sort: 排序
|
||||
gist.list.sort-by-created: 创建
|
||||
gist.list.sort-by-updated: 更新
|
||||
gist.list.order-by-asc: 最早
|
||||
gist.list.order-by-desc: 最近
|
||||
gist.list.select-tab: 选择一个标签
|
||||
gist.list.liked: 已喜欢
|
||||
gist.list.likes: 喜欢
|
||||
gist.list.forked: 已派生
|
||||
gist.list.forked-from: 派生自
|
||||
gist.list.forks: 派生
|
||||
gist.list.files: 文件
|
||||
gist.list.last-active: 最后活跃于
|
||||
gist.list.no-gists: 没有 Gist
|
||||
|
||||
gist.forks: 派生
|
||||
gist.forks.view: 查看派生
|
||||
gist.forks.no: 无公开派生
|
||||
|
||||
gist.likes: 喜欢
|
||||
gist.likes.no: 还没有喜欢
|
||||
|
||||
gist.revisions: 修订
|
||||
gist.revision.revised: 修订了这个 Gist
|
||||
gist.revision.go-to-revision: 跳至此修订
|
||||
gist.revision.file-created: file created
|
||||
gist.revision.file-deleted: file deleted
|
||||
gist.revision.file-renamed: 重命名为
|
||||
gist.revision.diff-truncated: 由于变更差异过大,显示内容已被截断
|
||||
gist.revision.file-renamed-no-changes: File renamed without changes
|
||||
gist.revision.empty-file: 空文件
|
||||
gist.revision.no-changes: 没有变更
|
||||
gist.revision.no-revisions: 无可供显示的修订
|
||||
|
||||
settings: 设置
|
||||
settings.email: 邮箱
|
||||
settings.email-help: 用于提交与 Gravatar
|
||||
settings.email-set: 设置邮箱地址
|
||||
settings.link-accounts: 关联账号
|
||||
settings.link-github-account: 关联 GitHub 账号
|
||||
settings.link-gitea-account: 关联 Gitea 账号
|
||||
settings.unlink-github-account: 解除关联 GitHub 账号
|
||||
settings.unlink-gitea-account: 解除关联 Gitea 账号
|
||||
settings.delete-account: 删除账号
|
||||
settings.delete-account-confirm: 您确认要删除您的账号吗?
|
||||
settings.add-ssh-key: 添加 SSH 密钥
|
||||
settings.add-ssh-key-help: 用于使用 Git 通过 SSH 拉取与推送 Gist
|
||||
settings.add-ssh-key-title: 标题
|
||||
settings.add-ssh-key-content: 密钥
|
||||
settings.delete-ssh-key: 删除
|
||||
settings.delete-ssh-key-confirm: Confirm deletion of SSH key
|
||||
settings.ssh-key-added-at: 添加
|
||||
settings.ssh-key-never-used: 从未使用过
|
||||
settings.ssh-key-last-used: 最后使用于
|
||||
|
||||
auth.signup-disabled: 管理员已禁用了注册
|
||||
auth.login: 登录
|
||||
auth.signup: 注册
|
||||
auth.new-account: 新建账号
|
||||
auth.username: 用户名
|
||||
auth.password: 密码
|
||||
auth.register-instead: 转到注册
|
||||
auth.login-instead: 转到登录
|
||||
auth.oauth: 使用 %s 账号继续
|
||||
|
||||
error: 错误
|
||||
|
||||
header.menu.all: 全部
|
||||
header.menu.new: 创建
|
||||
header.menu.search: 搜索
|
||||
header.menu.my-gists: 我的 Gists
|
||||
header.menu.liked: Liked
|
||||
header.menu.admin: 管理
|
||||
header.menu.settings: 设置
|
||||
header.menu.logout: 登出
|
||||
header.menu.register: 注册
|
||||
header.menu.login: 登录
|
||||
header.menu.light: 亮色
|
||||
header.menu.dark: 暗色
|
||||
header.menu.system: 系统
|
||||
footer.powered-by: 由 %s 强力驱动
|
||||
|
||||
pagination.older: 更早
|
||||
pagination.newer: 更新
|
||||
pagination.previous: 上一页
|
||||
pagination.next: 下一页
|
||||
|
||||
admin.admin_panel: 管理面板
|
||||
admin.general: 通用
|
||||
admin.users: 用户
|
||||
admin.gists: Gists
|
||||
admin.configuration: 配置
|
||||
admin.versions: 版本
|
||||
admin.ssh_keys: SSH 密钥
|
||||
admin.stats: 状态
|
||||
admin.actions: 动作
|
||||
admin.actions.sync-fs: 从文件系统同步 Gist
|
||||
admin.actions.sync-db: 从数据库同步 Gist
|
||||
admin.actions.git-gc: 对 Git 仓库执行垃圾回收
|
||||
admin.id: ID
|
||||
admin.user: 用户
|
||||
admin.delete: 删除
|
||||
admin.created_at: 创建于
|
||||
|
||||
admin.config-link: 此配置可通过 YAML 配置和/或环境变量进行 %s 。
|
||||
admin.config-link-overriden: 覆盖
|
||||
admin.disable-signup: 禁用注册
|
||||
admin.disable-signup_help: 阻止创建新的账号。
|
||||
admin.require-login: 要求登录
|
||||
admin.require-login_help: 强制用户登录后才能查看 Gist。
|
||||
admin.disable-login: 禁用登录表单
|
||||
admin.disable-login_help: 禁止使用登录表单进行登录以强制通过 OAuth 提供方登录。
|
||||
admin.disable-gravatar: 禁用 Gravatar
|
||||
admin.disable-gravatar_help: 停止使用 Gravatar 作为头像提供方。
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: 你想要删除此用户吗?
|
||||
|
||||
admin.gists.title: 标题
|
||||
admin.gists.private: 私有?
|
||||
admin.gists.nb-files: 文件数
|
||||
admin.gists.nb-likes: 喜欢数
|
||||
admin.gists.delete_confirm: 你想要删除此 Gist 吗?
|
||||
gist.new.url: 'URL'
|
||||
gist.new.preview: ''
|
||||
error.page-not-found: ''
|
||||
gist.new.create-a-new-gist: '创建一个新的gist'
|
||||
gist.edit.edit-gist: ''
|
||||
gist.list.all-liked-by: ''
|
||||
gist.list.all-forked-by: ''
|
||||
gist.list.all-from: ''
|
||||
gist.search.found: ''
|
||||
gist.search.no-results: '没有找到gist'
|
||||
gist.search.help.user: '由用户创建的gist'
|
||||
gist.search.help.title: '给定标题的gist'
|
||||
gist.search.help.filename: ''
|
||||
gist.search.help.extension: ''
|
||||
gist.search.help.language: ''
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
settings.link-gitlab-account: ''
|
||||
settings.unlink-gitlab-account: ''
|
||||
settings.change-username: ''
|
||||
settings.create-password: ''
|
||||
settings.create-password-help: ''
|
||||
settings.change-password: ''
|
||||
settings.change-password-help: ''
|
||||
settings.password-label-title: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.actions.sync-previews: ''
|
||||
admin.actions.reset-hooks: ''
|
||||
admin.actions.index-gists: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-be-empty: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
259
internal/i18n/locales/zh-TW.yml
Normal file
259
internal/i18n/locales/zh-TW.yml
Normal file
@@ -0,0 +1,259 @@
|
||||
gist.public: 公開
|
||||
gist.unlisted: 非公開
|
||||
gist.private: 私人
|
||||
|
||||
gist.header.like: 喜歡
|
||||
gist.header.unlike: 不喜歡
|
||||
gist.header.fork: 分支
|
||||
gist.header.edit: 編輯
|
||||
gist.header.delete: 刪除
|
||||
gist.header.forked-from: 分支自
|
||||
gist.header.last-active: 最後活躍
|
||||
gist.header.select-tab: 選擇分頁
|
||||
gist.header.code: 程式碼
|
||||
gist.header.revisions: 修訂記錄
|
||||
gist.header.revision: 修訂
|
||||
gist.header.clone-http: 透過 %s 複製
|
||||
gist.header.clone-http-help: 使用 HTTP 基本認證透過 Git 複製。
|
||||
gist.header.clone-ssh: 透過 SSH 複製
|
||||
gist.header.clone-ssh-help: 使用 SSH 金鑰透過 Git 複製。
|
||||
gist.header.embed: 嵌入
|
||||
gist.header.embed-help: 將這個 Gist 嵌入您的網站。
|
||||
gist.header.download-zip: 下載 ZIP
|
||||
|
||||
gist.raw: 原始檔案
|
||||
gist.file-truncated: 此檔案已被截斷。
|
||||
gist.watch-full-file: 查看完整檔案。
|
||||
gist.file-not-valid: 此檔案不是有效的 CSV 檔案。
|
||||
gist.no-content: 內容為空
|
||||
|
||||
gist.new.new_gist: 新增 Gist
|
||||
gist.new.title: 標題
|
||||
gist.new.description: 描述
|
||||
gist.new.url: 自定義 Gist 網址
|
||||
gist.new.filename-with-extension: 含副檔名的檔案名稱
|
||||
gist.new.indent-mode: 縮排模式
|
||||
gist.new.indent-mode-space: 空格
|
||||
gist.new.indent-mode-tab: tab
|
||||
gist.new.indent-size: 縮排寬度
|
||||
gist.new.wrap-mode: 換行模式
|
||||
gist.new.wrap-mode-no: 不換行
|
||||
gist.new.wrap-mode-soft: 自動換行
|
||||
gist.new.add-file: 新增檔案
|
||||
gist.new.create-public-button: 創建公開 Gist
|
||||
gist.new.create-unlisted-button: 創建非公開 Gist
|
||||
gist.new.create-private-button: 創建私人 Gist
|
||||
|
||||
gist.edit.editing: 編輯中
|
||||
gist.edit.change-visibility: 更改可見性
|
||||
gist.edit.delete: 刪除
|
||||
gist.edit.cancel: 取消
|
||||
gist.edit.save: 保存
|
||||
|
||||
gist.list.joined: 加入
|
||||
gist.list.all: 所有 Gists
|
||||
gist.list.search-results: 搜索結果
|
||||
gist.list.sort: 排序
|
||||
gist.list.sort-by-created: 創建
|
||||
gist.list.sort-by-updated: 更新
|
||||
gist.list.order-by-asc: 順序排序
|
||||
gist.list.order-by-desc: 倒序排序
|
||||
gist.list.select-tab: 選擇分頁
|
||||
gist.list.liked: 喜歡的 Gists
|
||||
gist.list.likes: 喜歡
|
||||
gist.list.forked: 分支
|
||||
gist.list.forked-from: 分支自
|
||||
gist.list.forks: 分支
|
||||
gist.list.files: 檔案
|
||||
gist.list.last-active: 最後活躍
|
||||
gist.list.no-gists: 沒有任何的 Gist
|
||||
|
||||
gist.forks: 分支
|
||||
gist.forks.view: 查看分支
|
||||
gist.forks.no: 沒有任何公開的分支
|
||||
|
||||
gist.likes: 喜歡
|
||||
gist.likes.no: 目前還沒有任何人喜歡
|
||||
|
||||
gist.revisions: 修訂版本
|
||||
gist.revision.revised: 已修改
|
||||
gist.revision.go-to-revision: 還原成這個修訂版本
|
||||
gist.revision.file-created: 檔案已創建
|
||||
gist.revision.file-deleted: 檔案已刪除
|
||||
gist.revision.file-renamed: 重命名為
|
||||
gist.revision.diff-truncated: 差異太大無法顯示
|
||||
gist.revision.file-renamed-no-changes: 檔案名稱與重新命名前相同
|
||||
gist.revision.empty-file: 檔案為空
|
||||
gist.revision.no-changes: 沒有任何變更
|
||||
gist.revision.no-revisions: 沒有任何修訂版可顯示
|
||||
|
||||
settings: 設定
|
||||
settings.email: 電子郵件
|
||||
settings.email-help: 用於提交和 Gravatar
|
||||
settings.email-set: 設定電子郵件
|
||||
settings.link-accounts: 連結帳號
|
||||
settings.link-github-account: 連結 GitHub 帳號
|
||||
settings.link-gitlab-account: 連結 Gitlab 帳號
|
||||
settings.link-gitea-account: 連結 Gitea 帳號
|
||||
settings.unlink-github-account: 取消連結 GitHub 帳號
|
||||
settings.unlink-gitlab-account: 取消連結 GitLab 帳號
|
||||
settings.unlink-gitea-account: 取消連結 Gitea 帳號
|
||||
settings.delete-account: 刪除帳號
|
||||
settings.delete-account-confirm: 確定要刪除您的帳號嗎?
|
||||
settings.add-ssh-key: 添加 SSH 金鑰
|
||||
settings.add-ssh-key-help: 僅用於藉由 SSH 使用 Git 拉取/推送 Gist
|
||||
settings.add-ssh-key-title: 名稱
|
||||
settings.add-ssh-key-content: 金鑰
|
||||
settings.delete-ssh-key: 刪除
|
||||
settings.delete-ssh-key-confirm: 確認刪除 SSH 金鑰
|
||||
settings.ssh-key-added-at: 添加於
|
||||
settings.ssh-key-never-used: 從未使用
|
||||
settings.ssh-key-last-used: 最後使用
|
||||
settings.change-username: 變更使用者名稱
|
||||
settings.create-password: 創建密碼
|
||||
settings.create-password-help: 創建您的密碼以通過 HTTP 登錄到 Opengist
|
||||
settings.change-password: 更改密碼
|
||||
settings.change-password-help: 更改您的密碼以通過 HTTP 登錄到 Opengist
|
||||
settings.password-label-title: 密碼
|
||||
|
||||
auth.signup-disabled: 管理員已禁用註冊
|
||||
auth.login: 登錄
|
||||
auth.signup: 註冊
|
||||
auth.new-account: 新增帳號
|
||||
auth.username: 使用者名稱
|
||||
auth.password: 密碼
|
||||
auth.register-instead: 註冊
|
||||
auth.login-instead: 登錄
|
||||
auth.oauth: 用 %s 帳號繼續
|
||||
|
||||
error: 錯誤
|
||||
|
||||
header.menu.all: 全部
|
||||
header.menu.new: 新建
|
||||
header.menu.search: 搜索
|
||||
header.menu.my-gists: 我的 Gists
|
||||
header.menu.liked: 喜歡的 Gists
|
||||
header.menu.admin: 管理
|
||||
header.menu.settings: 設定
|
||||
header.menu.logout: 登出
|
||||
header.menu.register: 註冊
|
||||
header.menu.login: 登錄
|
||||
header.menu.light: 亮色
|
||||
header.menu.dark: 暗色
|
||||
header.menu.system: 系統
|
||||
footer.powered-by: 由 %s 提供支持
|
||||
|
||||
pagination.older: 下一頁
|
||||
pagination.newer: 上一頁
|
||||
pagination.previous: 上一頁
|
||||
pagination.next: 下一頁
|
||||
|
||||
admin.admin_panel: 管理儀表板
|
||||
admin.general: 一般
|
||||
admin.users: 使用者
|
||||
admin.gists: Gists
|
||||
admin.configuration: 設定
|
||||
admin.versions: 版本
|
||||
admin.ssh_keys: SSH 金鑰
|
||||
admin.stats: 統計
|
||||
admin.actions: 操作
|
||||
admin.actions.sync-fs: 從系統同步 Gists
|
||||
admin.actions.sync-db: 從資料庫同步 Gists
|
||||
admin.actions.git-gc: 清理所有的 git 儲存庫
|
||||
admin.actions.sync-previews: 同步所有 Gists 預覽
|
||||
admin.actions.reset-hooks: 重置 Git 伺服器所有儲存庫的 Git hooks
|
||||
admin.id: ID
|
||||
admin.user: 使用者
|
||||
admin.delete: 刪除
|
||||
admin.created_at: 創建時間
|
||||
|
||||
admin.config-link: 這裡的設定可以通過 YAML 配置檔案或是環境變數 %s。
|
||||
admin.config-link-overriden: 覆蓋
|
||||
admin.disable-signup: 關閉註冊
|
||||
admin.disable-signup_help: 禁止創建新帳號。
|
||||
admin.require-login: 登錄後瀏覽
|
||||
admin.require-login_help: 強制使用者登錄以查看 Gist。
|
||||
admin.disable-login: 關閉登錄頁面
|
||||
admin.disable-login_help: 關閉通過登錄頁面登錄,強制使用 OAuth 提供者。
|
||||
admin.disable-gravatar: 禁用 Gravatar
|
||||
admin.disable-gravatar_help: 禁止使用 Gravatar 作為頭像提供者。
|
||||
admin.allow-gists-without-login:
|
||||
admin.allow-gists-without-login_help:
|
||||
admin.users.delete_confirm: 您要刪除這個使用者嗎?
|
||||
|
||||
admin.gists.title: 標題
|
||||
admin.gists.private: 是否為私人
|
||||
admin.gists.nb-files: 檔案數
|
||||
admin.gists.nb-likes: 喜歡
|
||||
admin.gists.delete_confirm: 您要刪除這個 Gist 嗎?
|
||||
gist.search.no-results: 沒有找到任何 Gists
|
||||
gist.search.help.title: Gists 的標題
|
||||
gist.search.help.filename: Gists 的檔案名稱
|
||||
gist.search.help.language: Gists 的程式語言
|
||||
admin.actions.index-gists: 索引所有的 Gists
|
||||
gist.search.help.user: 由使用者建立的 Gists
|
||||
gist.search.found: 已找到 Gists
|
||||
gist.search.help.extension: Gists 的副檔名
|
||||
gist.new.preview: ''
|
||||
gist.new.create-a-new-gist: ''
|
||||
gist.edit.edit-gist: ''
|
||||
gist.list.all-liked-by: ''
|
||||
gist.list.all-forked-by: ''
|
||||
gist.list.all-from: ''
|
||||
gist.forks.for: ''
|
||||
gist.likes.for: ''
|
||||
gist.revision-of: ''
|
||||
error.page-not-found: ''
|
||||
error.bad-request: ''
|
||||
error.signup-disabled: ''
|
||||
error.signup-disabled-form: ''
|
||||
error.login-disabled-form: ''
|
||||
error.complete-oauth-login: ''
|
||||
error.oauth-unsupported: ''
|
||||
error.cannot-bind-data: ''
|
||||
error.invalid-number: ''
|
||||
error.invalid-character-unescaped: ''
|
||||
admin.invitations: ''
|
||||
admin.invitations.create: ''
|
||||
admin.invitations.help: ''
|
||||
admin.invitations.max_uses: ''
|
||||
admin.invitations.expires_at: ''
|
||||
admin.invitations.code: ''
|
||||
admin.invitations.copy_link: ''
|
||||
admin.invitations.uses: ''
|
||||
admin.invitations.expired: ''
|
||||
flash.admin.user-deleted: ''
|
||||
flash.admin.gist-deleted: ''
|
||||
flash.admin.invitation-created: ''
|
||||
flash.admin.invitation-deleted: ''
|
||||
flash.admin.sync-fs: ''
|
||||
flash.admin.sync-db: ''
|
||||
flash.admin.git-gc: ''
|
||||
flash.admin.sync-previews: ''
|
||||
flash.admin.reset-hooks: ''
|
||||
flash.admin.index-gists: ''
|
||||
flash.auth.username-exists: ''
|
||||
flash.auth.invalid-credentials: ''
|
||||
flash.auth.account-linked-oauth: ''
|
||||
flash.auth.account-unlinked-oauth: ''
|
||||
flash.auth.user-sshkeys-not-retrievable: ''
|
||||
flash.auth.user-sshkeys-not-created: ''
|
||||
flash.auth.must-be-logged-in: ''
|
||||
flash.gist.visibility-changed: ''
|
||||
flash.gist.deleted: ''
|
||||
flash.gist.fork-own-gist: ''
|
||||
flash.gist.forked: ''
|
||||
flash.user.email-updated: ''
|
||||
flash.user.invalid-ssh-key: ''
|
||||
flash.user.ssh-key-added: ''
|
||||
flash.user.ssh-key-deleted: ''
|
||||
flash.user.password-updated: ''
|
||||
flash.user.username-updated: ''
|
||||
validation.is-too-long: ''
|
||||
validation.should-not-be-empty: ''
|
||||
validation.should-not-include-sub-directory: ''
|
||||
validation.should-only-contain-alphanumeric-characters: ''
|
||||
validation.should-only-contain-alphanumeric-characters-and-dashes: ''
|
||||
validation.not-enough: ''
|
||||
validation.invalid: ''
|
||||
html.title.admin-panel: ''
|
||||
157
internal/index/bleve.go
Normal file
157
internal/index/bleve.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/camelcase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
|
||||
"github.com/blevesearch/bleve/v2/search/query"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var bleveIndex bleve.Index
|
||||
|
||||
func Enabled() bool {
|
||||
return config.C.IndexEnabled
|
||||
}
|
||||
|
||||
func Open(indexFilename string) error {
|
||||
var err error
|
||||
bleveIndex, err = bleve.Open(indexFilename)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, bleve.ErrorIndexPathDoesNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
docMapping := bleve.NewDocumentMapping()
|
||||
docMapping.AddFieldMappingsAt("GistID", bleve.NewNumericFieldMapping())
|
||||
docMapping.AddFieldMappingsAt("Content", bleve.NewTextFieldMapping())
|
||||
|
||||
mapping := bleve.NewIndexMapping()
|
||||
|
||||
if err = mapping.AddCustomTokenFilter("unicodeNormalize", map[string]any{
|
||||
"type": unicodenorm.Name,
|
||||
"form": unicodenorm.NFC,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = mapping.AddCustomAnalyzer("gistAnalyser", map[string]interface{}{
|
||||
"type": custom.Name,
|
||||
"char_filters": []string{},
|
||||
"tokenizer": unicode.Name,
|
||||
"token_filters": []string{"unicodeNormalize", camelcase.Name, lowercase.Name},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
docMapping.DefaultAnalyzer = "gistAnalyser"
|
||||
|
||||
bleveIndex, err = bleve.New(indexFilename, mapping)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
return bleveIndex.Close()
|
||||
}
|
||||
|
||||
func AddInIndex(gist *Gist) error {
|
||||
if !Enabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if gist == nil {
|
||||
return errors.New("failed to add nil gist to index")
|
||||
}
|
||||
return bleveIndex.Index(strconv.Itoa(int(gist.GistID)), gist)
|
||||
}
|
||||
|
||||
func RemoveFromIndex(gistID uint) error {
|
||||
if !Enabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bleveIndex.Delete(strconv.Itoa(int(gistID)))
|
||||
}
|
||||
|
||||
func SearchGists(queryStr string, queryMetadata SearchGistMetadata, gistsIds []uint, page int) ([]uint, uint64, map[string]int, error) {
|
||||
if !Enabled() {
|
||||
return nil, 0, nil, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var indexerQuery query.Query
|
||||
if queryStr != "" {
|
||||
contentQuery := bleve.NewMatchPhraseQuery(queryStr)
|
||||
contentQuery.FieldVal = "Content"
|
||||
indexerQuery = contentQuery
|
||||
} else {
|
||||
contentQuery := bleve.NewMatchAllQuery()
|
||||
indexerQuery = contentQuery
|
||||
}
|
||||
|
||||
if len(gistsIds) > 0 {
|
||||
repoQueries := make([]query.Query, 0, len(gistsIds))
|
||||
|
||||
truee := true
|
||||
for _, id := range gistsIds {
|
||||
f := float64(id)
|
||||
qq := bleve.NewNumericRangeInclusiveQuery(&f, &f, &truee, &truee)
|
||||
qq.SetField("GistID")
|
||||
repoQueries = append(repoQueries, qq)
|
||||
}
|
||||
|
||||
indexerQuery = bleve.NewConjunctionQuery(bleve.NewDisjunctionQuery(repoQueries...), indexerQuery)
|
||||
}
|
||||
|
||||
addQuery := func(field, value string) {
|
||||
if value != "" && value != "." {
|
||||
q := bleve.NewMatchPhraseQuery(value)
|
||||
q.FieldVal = field
|
||||
indexerQuery = bleve.NewConjunctionQuery(indexerQuery, q)
|
||||
}
|
||||
}
|
||||
|
||||
addQuery("Username", queryMetadata.Username)
|
||||
addQuery("Title", queryMetadata.Title)
|
||||
addQuery("Extensions", "."+queryMetadata.Extension)
|
||||
addQuery("Filenames", queryMetadata.Filename)
|
||||
addQuery("Languages", queryMetadata.Language)
|
||||
|
||||
languageFacet := bleve.NewFacetRequest("Languages", 10)
|
||||
|
||||
perPage := 10
|
||||
offset := (page - 1) * perPage
|
||||
|
||||
s := bleve.NewSearchRequestOptions(indexerQuery, perPage, offset, false)
|
||||
s.AddFacet("languageFacet", languageFacet)
|
||||
s.Fields = []string{"GistID"}
|
||||
s.IncludeLocations = false
|
||||
|
||||
results, err := bleveIndex.Search(s)
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
|
||||
gistIds := make([]uint, 0, len(results.Hits))
|
||||
for _, hit := range results.Hits {
|
||||
gistIds = append(gistIds, uint(hit.Fields["GistID"].(float64)))
|
||||
}
|
||||
|
||||
languageCounts := make(map[string]int)
|
||||
if facets, found := results.Facets["languageFacet"]; found {
|
||||
for _, term := range facets.Terms.Terms() {
|
||||
languageCounts[term.Term] = term.Count
|
||||
}
|
||||
}
|
||||
|
||||
return gistIds, results.Total, languageCounts, nil
|
||||
}
|
||||
21
internal/index/gist.go
Normal file
21
internal/index/gist.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package index
|
||||
|
||||
type Gist struct {
|
||||
GistID uint
|
||||
Username string
|
||||
Title string
|
||||
Content string
|
||||
Filenames []string
|
||||
Extensions []string
|
||||
Languages []string
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
type SearchGistMetadata struct {
|
||||
Username string
|
||||
Title string
|
||||
Filename string
|
||||
Extension string
|
||||
Language string
|
||||
}
|
||||
72
internal/memdb/memdb.go
Normal file
72
internal/memdb/memdb.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package memdb
|
||||
|
||||
import "github.com/hashicorp/go-memdb"
|
||||
import ogdb "github.com/thomiceli/opengist/internal/db"
|
||||
|
||||
var db *memdb.MemDB
|
||||
|
||||
type GistInit struct {
|
||||
UserID uint
|
||||
Gist *ogdb.Gist
|
||||
}
|
||||
|
||||
func Setup() error {
|
||||
var err error
|
||||
schema := &memdb.DBSchema{
|
||||
Tables: map[string]*memdb.TableSchema{
|
||||
"gist_init": {
|
||||
Name: "gist_init",
|
||||
Indexes: map[string]*memdb.IndexSchema{
|
||||
"id": {
|
||||
Name: "id",
|
||||
Unique: true,
|
||||
Indexer: &memdb.UintFieldIndex{Field: "UserID"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
db, err = memdb.NewMemDB(schema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InsertGistInit(userId uint, gist *ogdb.Gist) error {
|
||||
txn := db.Txn(true)
|
||||
if err := txn.Insert("gist_init", &GistInit{
|
||||
UserID: userId,
|
||||
Gist: gist,
|
||||
}); err != nil {
|
||||
txn.Abort()
|
||||
return err
|
||||
}
|
||||
|
||||
txn.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetGistInitAndDelete(userId uint) (*GistInit, error) {
|
||||
txn := db.Txn(true)
|
||||
defer txn.Abort()
|
||||
|
||||
raw, err := txn.First("gist_init", "id", userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gistInit := raw.(*GistInit)
|
||||
if err := txn.Delete("gist_init", gistInit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txn.Commit()
|
||||
return gistInit, nil
|
||||
}
|
||||
168
internal/render/highlight.go
Normal file
168
internal/render/highlight.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type RenderedFile struct {
|
||||
*git.File
|
||||
Type string `json:"type"`
|
||||
Lines []string `json:"-"`
|
||||
HTML string `json:"-"`
|
||||
}
|
||||
|
||||
type RenderedGist struct {
|
||||
*db.Gist
|
||||
Lines []string
|
||||
HTML string
|
||||
}
|
||||
|
||||
func HighlightFile(file *git.File) (RenderedFile, error) {
|
||||
rendered := RenderedFile{
|
||||
File: file,
|
||||
}
|
||||
|
||||
style := newStyle()
|
||||
lexer := newLexer(file.Filename)
|
||||
if lexer.Config().Name == "markdown" {
|
||||
return MarkdownFile(file)
|
||||
}
|
||||
|
||||
formatter := html.New(html.WithClasses(true), html.PreventSurroundingPre(true))
|
||||
|
||||
iterator, err := lexer.Tokenise(nil, file.Content+"\n")
|
||||
if err != nil {
|
||||
return rendered, err
|
||||
}
|
||||
|
||||
htmlbuf := bytes.Buffer{}
|
||||
w := bufio.NewWriter(&htmlbuf)
|
||||
|
||||
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
|
||||
lines := make([]string, 0, len(tokensLines))
|
||||
for _, tokens := range tokensLines {
|
||||
iterator = chroma.Literator(tokens...)
|
||||
err = formatter.Format(&htmlbuf, style, iterator)
|
||||
if err != nil {
|
||||
return rendered, fmt.Errorf("unable to format code: %w", err)
|
||||
}
|
||||
lines = append(lines, htmlbuf.String())
|
||||
htmlbuf.Reset()
|
||||
}
|
||||
|
||||
_ = w.Flush()
|
||||
|
||||
rendered.Lines = lines
|
||||
rendered.Type = parseFileTypeName(*lexer.Config())
|
||||
|
||||
return rendered, err
|
||||
}
|
||||
|
||||
func HighlightFiles(files []*git.File) []RenderedFile {
|
||||
const numWorkers = 10
|
||||
jobs := make(chan int, numWorkers)
|
||||
renderedFiles := make([]RenderedFile, len(files))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
worker := func() {
|
||||
for idx := range jobs {
|
||||
rendered, err := HighlightFile(files[idx])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error rendering gist preview for " + files[idx].Filename)
|
||||
}
|
||||
renderedFiles[idx] = rendered
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go worker()
|
||||
}
|
||||
|
||||
for i := range files {
|
||||
jobs <- i
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return renderedFiles
|
||||
}
|
||||
|
||||
func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) {
|
||||
rendered := RenderedGist{
|
||||
Gist: gist,
|
||||
}
|
||||
|
||||
style := newStyle()
|
||||
lexer := newLexer(gist.PreviewFilename)
|
||||
if lexer.Config().Name == "markdown" {
|
||||
return MarkdownGistPreview(gist)
|
||||
}
|
||||
|
||||
formatter := html.New(html.WithClasses(true), html.PreventSurroundingPre(true))
|
||||
|
||||
iterator, err := lexer.Tokenise(nil, gist.Preview)
|
||||
if err != nil {
|
||||
return rendered, err
|
||||
}
|
||||
|
||||
htmlbuf := bytes.Buffer{}
|
||||
w := bufio.NewWriter(&htmlbuf)
|
||||
|
||||
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
|
||||
lines := make([]string, 0, len(tokensLines))
|
||||
for _, tokens := range tokensLines {
|
||||
iterator = chroma.Literator(tokens...)
|
||||
err = formatter.Format(&htmlbuf, style, iterator)
|
||||
if err != nil {
|
||||
return rendered, fmt.Errorf("unable to format code: %w", err)
|
||||
}
|
||||
lines = append(lines, htmlbuf.String())
|
||||
htmlbuf.Reset()
|
||||
}
|
||||
|
||||
_ = w.Flush()
|
||||
|
||||
rendered.Lines = lines
|
||||
|
||||
return rendered, err
|
||||
}
|
||||
|
||||
func parseFileTypeName(config chroma.Config) string {
|
||||
fileType := config.Name
|
||||
if fileType == "fallback" || fileType == "plaintext" {
|
||||
return "Text"
|
||||
}
|
||||
|
||||
return fileType
|
||||
}
|
||||
|
||||
func newLexer(filename string) chroma.Lexer {
|
||||
var lexer chroma.Lexer
|
||||
if lexer = lexers.Get(filename); lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
return lexer
|
||||
}
|
||||
|
||||
func newStyle() *chroma.Style {
|
||||
var style *chroma.Style
|
||||
if style = styles.Get("catppuccin-latte"); style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
||||
119
internal/render/markdown.go
Normal file
119
internal/render/markdown.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"github.com/Kunde21/markdownfmt/v3"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/yuin/goldmark"
|
||||
emoji "github.com/yuin/goldmark-emoji"
|
||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
astex "github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"go.abhg.dev/goldmark/mermaid"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func MarkdownGistPreview(gist *db.Gist) (RenderedGist, error) {
|
||||
var buf bytes.Buffer
|
||||
err := newMarkdown().Convert([]byte(gist.Preview), &buf)
|
||||
|
||||
return RenderedGist{
|
||||
Gist: gist,
|
||||
HTML: buf.String(),
|
||||
}, err
|
||||
}
|
||||
|
||||
func MarkdownFile(file *git.File) (RenderedFile, error) {
|
||||
var buf bytes.Buffer
|
||||
err := newMarkdown().Convert([]byte(file.Content), &buf)
|
||||
|
||||
return RenderedFile{
|
||||
File: file,
|
||||
HTML: buf.String(),
|
||||
Type: "Markdown",
|
||||
}, err
|
||||
}
|
||||
func MarkdownString(content string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
err := newMarkdown().Convert([]byte(content), &buf)
|
||||
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func newMarkdown() goldmark.Markdown {
|
||||
return goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle("catppuccin-latte"),
|
||||
highlighting.WithFormatOptions(html.WithClasses(true))),
|
||||
emoji.Emoji,
|
||||
&mermaid.Extender{},
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(&CheckboxTransformer{}, 10000),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type CheckboxTransformer struct{}
|
||||
|
||||
func (t *CheckboxTransformer) Transform(node *ast.Document, _ text.Reader, _ parser.Context) {
|
||||
i := 1
|
||||
err := ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
if _, ok := n.(*astex.TaskCheckBox); ok {
|
||||
listitem := n.Parent().Parent()
|
||||
listitem.SetAttribute([]byte("data-checkbox-nb"), []byte(strconv.Itoa(i)))
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Checkbox(content string, checkboxNb int) (string, error) {
|
||||
buf := bytes.Buffer{}
|
||||
w := bufio.NewWriter(&buf)
|
||||
|
||||
source := []byte(content)
|
||||
markdown := markdownfmt.NewGoldmark()
|
||||
reader := text.NewReader(source)
|
||||
document := markdown.Parser().Parse(reader)
|
||||
|
||||
i := 1
|
||||
err := ast.Walk(document, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
if listItem, ok := n.(*astex.TaskCheckBox); ok {
|
||||
if i == checkboxNb {
|
||||
listItem.IsChecked = !listItem.IsChecked
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = markdown.Renderer().Render(w, source, document); err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -2,14 +2,16 @@ package ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/auth"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
|
||||
@@ -32,27 +34,43 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
|
||||
userName := strings.ToLower(repoFields[0])
|
||||
gistName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git")
|
||||
|
||||
gist, err := models.GetGist(userName, gistName)
|
||||
gist, err := db.GetGist(userName, gistName)
|
||||
if err != nil {
|
||||
return errors.New("gist not found")
|
||||
}
|
||||
|
||||
requireLogin, err := models.GetSetting(models.SettingRequireLogin)
|
||||
allowUnauthenticated, err := auth.ShouldAllowUnauthenticatedGistAccess(db.DBAuthInfo{}, true)
|
||||
if err != nil {
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
|
||||
if verb == "receive-pack" || requireLogin == "1" {
|
||||
pubKey, err := models.SSHKeyExistsForUser(key, gist.UserID)
|
||||
// Check for the key if :
|
||||
// - user wants to push the gist
|
||||
// - user wants to clone a private gist
|
||||
// - gist is not found (obfuscation)
|
||||
// - admin setting to require login is set to true
|
||||
if verb == "receive-pack" ||
|
||||
gist.Private == db.PrivateVisibility ||
|
||||
gist.ID == 0 ||
|
||||
!allowUnauthenticated {
|
||||
|
||||
var userToCheckPermissions *db.User
|
||||
if gist.Private != db.PrivateVisibility && verb == "upload-pack" {
|
||||
userToCheckPermissions, _ = db.GetUserFromSSHKey(key)
|
||||
} else {
|
||||
userToCheckPermissions = &gist.User
|
||||
}
|
||||
|
||||
pubKey, err := db.SSHKeyExistsForUser(key, userToCheckPermissions.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)
|
||||
return errors.New("unauthorized")
|
||||
return errors.New("gist not found")
|
||||
}
|
||||
errorSsh("Failed to get user by SSH key id", err)
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
_ = models.SSHKeyLastUsedNow(pubKey.Content)
|
||||
_ = db.SSHKeyLastUsedNow(pubKey.Content)
|
||||
}
|
||||
|
||||
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
|
||||
@@ -85,7 +103,8 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
|
||||
// updatedAt is updated only if serviceType is receive-pack
|
||||
if verb == "receive-pack" {
|
||||
_ = gist.SetLastActiveNow()
|
||||
_ = gist.UpdatePreviewAndCount()
|
||||
_ = gist.UpdatePreviewAndCount(false)
|
||||
gist.AddInIndex()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
@@ -24,9 +24,9 @@ func Start() {
|
||||
sshConfig := &ssh.ServerConfig{
|
||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
|
||||
_, err := models.SSHKeyDoesExists(strKey)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
exists, err := db.SSHKeyDoesExists(strKey)
|
||||
if !exists || err != nil {
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func listen(serverConfig *ssh.ServerConfig) {
|
||||
go func() {
|
||||
sConn, channels, reqs, err := ssh.NewServerConn(nConn, serverConfig)
|
||||
if err != nil {
|
||||
if !(err != io.EOF && !errors.Is(err, syscall.ECONNRESET)) {
|
||||
if err != io.EOF && !errors.Is(err, syscall.ECONNRESET) {
|
||||
errorSsh("Failed to handshake", err)
|
||||
}
|
||||
return
|
||||
|
||||
76
internal/utils/argon2id.go
Normal file
76
internal/utils/argon2id.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Argon2ID struct {
|
||||
format string
|
||||
version int
|
||||
time uint32
|
||||
memory uint32
|
||||
keyLen uint32
|
||||
saltLen uint32
|
||||
threads uint8
|
||||
}
|
||||
|
||||
var Argon2id = Argon2ID{
|
||||
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
version: argon2.Version,
|
||||
time: 1,
|
||||
memory: 64 * 1024,
|
||||
keyLen: 32,
|
||||
saltLen: 16,
|
||||
threads: 4,
|
||||
}
|
||||
|
||||
func (a Argon2ID) Hash(plain string) (string, error) {
|
||||
salt := make([]byte, a.saltLen)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)
|
||||
|
||||
return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
|
||||
base64.RawStdEncoding.EncodeToString(salt),
|
||||
base64.RawStdEncoding.EncodeToString(hash),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (a Argon2ID) Verify(plain, hash string) (bool, error) {
|
||||
if hash == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
hashParts := strings.Split(hash, "$")
|
||||
|
||||
if len(hashParts) != 6 {
|
||||
return false, errors.New("invalid hash")
|
||||
}
|
||||
|
||||
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))
|
||||
|
||||
return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
|
||||
}
|
||||
26
internal/utils/session.go
Normal file
26
internal/utils/session.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/rs/zerolog/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func ReadKey(filePath string) []byte {
|
||||
key, err := os.ReadFile(filePath)
|
||||
if err == nil {
|
||||
return key
|
||||
}
|
||||
|
||||
key = securecookie.GenerateRandomKey(32)
|
||||
if key == nil {
|
||||
log.Fatal().Msg("Failed to generate a new key for sessions")
|
||||
}
|
||||
|
||||
err = os.WriteFile(filePath, key, 0600)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msgf("Failed to save the key to %s", filePath)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package utils
|
||||
|
||||
func SliceContains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
func RemoveDuplicates[T string | int](sliceList []T) []T {
|
||||
allKeys := make(map[T]bool)
|
||||
list := []T{}
|
||||
for _, item := range sliceList {
|
||||
if _, value := allKeys[item]; !value {
|
||||
allKeys[item] = true
|
||||
list = append(list, item)
|
||||
}
|
||||
}
|
||||
return false
|
||||
return list
|
||||
}
|
||||
|
||||
75
internal/utils/validator.go
Normal file
75
internal/utils/validator.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OpengistValidator struct {
|
||||
v *validator.Validate
|
||||
}
|
||||
|
||||
func NewValidator() *OpengistValidator {
|
||||
v := validator.New()
|
||||
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
|
||||
_ = v.RegisterValidation("alphanumdash", validateAlphaNumDash)
|
||||
_ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty)
|
||||
return &OpengistValidator{v}
|
||||
}
|
||||
|
||||
func (cv *OpengistValidator) Validate(i interface{}) error {
|
||||
return cv.v.Struct(i)
|
||||
}
|
||||
|
||||
func (cv *OpengistValidator) Var(field interface{}, tag string) error {
|
||||
return cv.v.Var(field, tag)
|
||||
}
|
||||
|
||||
func ValidationMessages(err *error, locale *i18n.Locale) string {
|
||||
errs := (*err).(validator.ValidationErrors)
|
||||
messages := make([]string, len(errs))
|
||||
for i, e := range errs {
|
||||
switch e.Tag() {
|
||||
case "max":
|
||||
messages[i] = locale.String("validation.is-too-long", e.Field())
|
||||
case "required":
|
||||
messages[i] = locale.String("validation.should-not-be-empty", e.Field())
|
||||
case "excludes":
|
||||
messages[i] = locale.String("validation.should-not-include-sub-directory", e.Field())
|
||||
case "alphanum":
|
||||
messages[i] = locale.String("validation.should-only-contain-alphanumeric-characters", e.Field())
|
||||
case "alphanumdash":
|
||||
case "alphanumdashorempty":
|
||||
messages[i] = locale.String("validation.should-only-contain-alphanumeric-characters-and-dashes", e.Field())
|
||||
case "min":
|
||||
messages[i] = locale.String("validation.not-enough", e.Field())
|
||||
case "notreserved":
|
||||
messages[i] = locale.String("validation.invalid", e.Field())
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(messages, " ; ")
|
||||
}
|
||||
|
||||
func validateReservedKeywords(fl validator.FieldLevel) bool {
|
||||
name := fl.Field().String()
|
||||
|
||||
restrictedNames := map[string]struct{}{}
|
||||
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck", "preview"} {
|
||||
restrictedNames[restrictedName] = struct{}{}
|
||||
}
|
||||
|
||||
// if the name is not in the restricted names, it is valid
|
||||
_, ok := restrictedNames[name]
|
||||
return !ok
|
||||
}
|
||||
|
||||
func validateAlphaNumDash(fl validator.FieldLevel) bool {
|
||||
return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
|
||||
}
|
||||
|
||||
func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool {
|
||||
return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
|
||||
}
|
||||
@@ -2,25 +2,17 @@ package web
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/actions"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
syncReposFromFS = false
|
||||
syncReposFromDB = false
|
||||
"time"
|
||||
)
|
||||
|
||||
func adminIndex(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Admin panel")
|
||||
setData(ctx, "htmlTitle", "Admin panel")
|
||||
setData(ctx, "htmlTitle", trH(ctx, "admin.admin_panel"))
|
||||
setData(ctx, "adminHeaderPage", "index")
|
||||
|
||||
setData(ctx, "opengistVersion", config.OpengistVersion)
|
||||
@@ -31,62 +23,64 @@ func adminIndex(ctx echo.Context) error {
|
||||
}
|
||||
setData(ctx, "gitVersion", gitVersion)
|
||||
|
||||
countUsers, err := models.CountAll(&models.User{})
|
||||
countUsers, err := db.CountAll(&db.User{})
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot count users", err)
|
||||
}
|
||||
setData(ctx, "countUsers", countUsers)
|
||||
|
||||
countGists, err := models.CountAll(&models.Gist{})
|
||||
countGists, err := db.CountAll(&db.Gist{})
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot count gists", err)
|
||||
}
|
||||
setData(ctx, "countGists", countGists)
|
||||
|
||||
countKeys, err := models.CountAll(&models.SSHKey{})
|
||||
countKeys, err := db.CountAll(&db.SSHKey{})
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot count SSH keys", err)
|
||||
}
|
||||
setData(ctx, "countKeys", countKeys)
|
||||
|
||||
setData(ctx, "syncReposFromFS", syncReposFromFS)
|
||||
setData(ctx, "syncReposFromDB", syncReposFromDB)
|
||||
setData(ctx, "syncReposFromFS", actions.IsRunning(actions.SyncReposFromFS))
|
||||
setData(ctx, "syncReposFromDB", actions.IsRunning(actions.SyncReposFromDB))
|
||||
setData(ctx, "gitGcRepos", actions.IsRunning(actions.GitGcRepos))
|
||||
setData(ctx, "syncGistPreviews", actions.IsRunning(actions.SyncGistPreviews))
|
||||
setData(ctx, "resetHooks", actions.IsRunning(actions.ResetHooks))
|
||||
setData(ctx, "indexGists", actions.IsRunning(actions.IndexGists))
|
||||
return html(ctx, "admin_index.html")
|
||||
}
|
||||
|
||||
func adminUsers(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Users")
|
||||
setData(ctx, "htmlTitle", "Users - Admin panel")
|
||||
setData(ctx, "htmlTitle", trH(ctx, "admin.users")+" - "+trH(ctx, "admin.admin_panel"))
|
||||
setData(ctx, "adminHeaderPage", "users")
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
var data []*models.User
|
||||
var data []*db.User
|
||||
var err error
|
||||
if data, err = models.GetAllUsers(pageInt - 1); err != nil {
|
||||
if data, err = db.GetAllUsers(pageInt - 1); err != nil {
|
||||
return errorRes(500, "Cannot get users", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
|
||||
}
|
||||
|
||||
return html(ctx, "admin_users.html")
|
||||
}
|
||||
|
||||
func adminGists(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Gists")
|
||||
setData(ctx, "htmlTitle", "Gists - Admin panel")
|
||||
setData(ctx, "htmlTitle", trH(ctx, "admin.gists")+" - "+trH(ctx, "admin.admin_panel"))
|
||||
setData(ctx, "adminHeaderPage", "gists")
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
var data []*models.Gist
|
||||
var data []*db.Gist
|
||||
var err error
|
||||
if data, err = models.GetAllGists(pageInt - 1); err != nil {
|
||||
if data, err = db.GetAllGists(pageInt - 1); err != nil {
|
||||
return errorRes(500, "Cannot get gists", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
|
||||
}
|
||||
|
||||
return html(ctx, "admin_gists.html")
|
||||
@@ -94,7 +88,7 @@ func adminGists(ctx echo.Context) error {
|
||||
|
||||
func adminUserDelete(ctx echo.Context) error {
|
||||
userId, _ := strconv.ParseUint(ctx.Param("user"), 10, 64)
|
||||
user, err := models.GetUserById(uint(userId))
|
||||
user, err := db.GetUserById(uint(userId))
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot retrieve user", err)
|
||||
}
|
||||
@@ -103,12 +97,12 @@ func adminUserDelete(ctx echo.Context) error {
|
||||
return errorRes(500, "Cannot delete this user", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "User has been deleted", "success")
|
||||
addFlash(ctx, tr(ctx, "flash.admin.user-deleted"), "success")
|
||||
return redirect(ctx, "/admin-panel/users")
|
||||
}
|
||||
|
||||
func adminGistDelete(ctx echo.Context) error {
|
||||
gist, err := models.GetGistByID(ctx.Param("gist"))
|
||||
gist, err := db.GetGistByID(ctx.Param("gist"))
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot retrieve gist", err)
|
||||
}
|
||||
@@ -121,73 +115,50 @@ func adminGistDelete(ctx echo.Context) error {
|
||||
return errorRes(500, "Cannot delete this gist", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "Gist has been deleted", "success")
|
||||
gist.RemoveFromIndex()
|
||||
|
||||
addFlash(ctx, tr(ctx, "flash.admin.gist-deleted"), "success")
|
||||
return redirect(ctx, "/admin-panel/gists")
|
||||
}
|
||||
|
||||
func adminSyncReposFromFS(ctx echo.Context) error {
|
||||
addFlash(ctx, "Syncing repositories from filesystem...", "success")
|
||||
go func() {
|
||||
if syncReposFromFS {
|
||||
return
|
||||
}
|
||||
syncReposFromFS = true
|
||||
|
||||
gists, err := models.GetAllGistsRows()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get gists")
|
||||
syncReposFromFS = false
|
||||
return
|
||||
}
|
||||
for _, gist := range gists {
|
||||
// if repository does not exist, delete gist from database
|
||||
if _, err := os.Stat(git.RepositoryPath(gist.User.Username, gist.Uuid)); err != nil && !os.IsExist(err) {
|
||||
if err2 := gist.Delete(); err2 != nil {
|
||||
log.Error().Err(err2).Msg("Cannot delete gist")
|
||||
syncReposFromFS = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
syncReposFromFS = false
|
||||
}()
|
||||
addFlash(ctx, tr(ctx, "flash.admin.sync-fs"), "success")
|
||||
go actions.Run(actions.SyncReposFromFS)
|
||||
return redirect(ctx, "/admin-panel")
|
||||
}
|
||||
|
||||
func adminSyncReposFromDB(ctx echo.Context) error {
|
||||
addFlash(ctx, "Syncing repositories from database...", "success")
|
||||
go func() {
|
||||
if syncReposFromDB {
|
||||
return
|
||||
}
|
||||
syncReposFromDB = true
|
||||
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot read repos directories")
|
||||
syncReposFromDB = false
|
||||
return
|
||||
}
|
||||
addFlash(ctx, tr(ctx, "flash.admin.sync-db"), "success")
|
||||
go actions.Run(actions.SyncReposFromDB)
|
||||
return redirect(ctx, "/admin-panel")
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
path := strings.Split(e, string(os.PathSeparator))
|
||||
gist, _ := models.GetGist(path[len(path)-2], path[len(path)-1])
|
||||
func adminGcRepos(ctx echo.Context) error {
|
||||
addFlash(ctx, tr(ctx, "flash.admin.git-gc"), "success")
|
||||
go actions.Run(actions.GitGcRepos)
|
||||
return redirect(ctx, "/admin-panel")
|
||||
}
|
||||
|
||||
if gist.ID == 0 {
|
||||
if err := git.DeleteRepository(path[len(path)-2], path[len(path)-1]); err != nil {
|
||||
log.Error().Err(err).Msg("Cannot delete repository")
|
||||
syncReposFromDB = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
syncReposFromDB = false
|
||||
}()
|
||||
func adminSyncGistPreviews(ctx echo.Context) error {
|
||||
addFlash(ctx, tr(ctx, "flash.admin.sync-previews"), "success")
|
||||
go actions.Run(actions.SyncGistPreviews)
|
||||
return redirect(ctx, "/admin-panel")
|
||||
}
|
||||
|
||||
func adminResetHooks(ctx echo.Context) error {
|
||||
addFlash(ctx, tr(ctx, "flash.admin.reset-hooks"), "success")
|
||||
go actions.Run(actions.ResetHooks)
|
||||
return redirect(ctx, "/admin-panel")
|
||||
}
|
||||
|
||||
func adminIndexGists(ctx echo.Context) error {
|
||||
addFlash(ctx, tr(ctx, "flash.admin.index-gists"), "success")
|
||||
go actions.Run(actions.IndexGists)
|
||||
return redirect(ctx, "/admin-panel")
|
||||
}
|
||||
|
||||
func adminConfig(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Configuration")
|
||||
setData(ctx, "htmlTitle", "Configuration - Admin panel")
|
||||
setData(ctx, "htmlTitle", trH(ctx, "admin.configuration")+" - "+trH(ctx, "admin.admin_panel"))
|
||||
setData(ctx, "adminHeaderPage", "config")
|
||||
|
||||
return html(ctx, "admin_config.html")
|
||||
@@ -197,7 +168,7 @@ func adminSetConfig(ctx echo.Context) error {
|
||||
key := ctx.FormValue("key")
|
||||
value := ctx.FormValue("value")
|
||||
|
||||
if err := models.UpdateSetting(key, value); err != nil {
|
||||
if err := db.UpdateSetting(key, value); err != nil {
|
||||
return errorRes(500, "Cannot set setting", err)
|
||||
}
|
||||
|
||||
@@ -205,3 +176,58 @@ func adminSetConfig(ctx echo.Context) error {
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
func adminInvitations(ctx echo.Context) error {
|
||||
setData(ctx, "htmlTitle", trH(ctx, "admin.invitations")+" - "+trH(ctx, "admin.admin_panel"))
|
||||
setData(ctx, "adminHeaderPage", "invitations")
|
||||
|
||||
var invitations []*db.Invitation
|
||||
var err error
|
||||
if invitations, err = db.GetAllInvitations(); err != nil {
|
||||
return errorRes(500, "Cannot get invites", err)
|
||||
}
|
||||
|
||||
setData(ctx, "invitations", invitations)
|
||||
return html(ctx, "admin_invitations.html")
|
||||
}
|
||||
|
||||
func adminInvitationsCreate(ctx echo.Context) error {
|
||||
code := ctx.FormValue("code")
|
||||
nbMax, err := strconv.ParseUint(ctx.FormValue("nbMax"), 10, 64)
|
||||
if err != nil {
|
||||
nbMax = 10
|
||||
}
|
||||
|
||||
expiresAtUnix, err := strconv.ParseInt(ctx.FormValue("expiredAtUnix"), 10, 64)
|
||||
if err != nil {
|
||||
expiresAtUnix = time.Now().Unix() + 604800 // 1 week
|
||||
}
|
||||
|
||||
invitation := &db.Invitation{
|
||||
Code: code,
|
||||
ExpiresAt: expiresAtUnix,
|
||||
NbMax: uint(nbMax),
|
||||
}
|
||||
|
||||
if err := invitation.Create(); err != nil {
|
||||
return errorRes(500, "Cannot create invitation", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, tr(ctx, "flash.admin.invitation-created"), "success")
|
||||
return redirect(ctx, "/admin-panel/invitations")
|
||||
}
|
||||
|
||||
func adminInvitationsDelete(ctx echo.Context) error {
|
||||
id, _ := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
invitation, err := db.GetInvitationByID(uint(id))
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot retrieve invitation", err)
|
||||
}
|
||||
|
||||
if err := invitation.Delete(); err != nil {
|
||||
return errorRes(500, "Cannot delete this invitation", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, tr(ctx, "flash.admin.invitation-deleted"), "success")
|
||||
return redirect(ctx, "/admin-panel/invitations")
|
||||
}
|
||||
|
||||
@@ -6,65 +6,99 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/gitea"
|
||||
"github.com/markbates/goth/providers/github"
|
||||
"github.com/markbates/goth/providers/gitlab"
|
||||
"github.com/markbates/goth/providers/openidConnect"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
GitHubProvider = "github"
|
||||
GitLabProvider = "gitlab"
|
||||
GiteaProvider = "gitea"
|
||||
OpenIDConnect = "openid-connect"
|
||||
)
|
||||
|
||||
var title = cases.Title(language.English)
|
||||
|
||||
func register(ctx echo.Context) error {
|
||||
setData(ctx, "title", "New account")
|
||||
setData(ctx, "htmlTitle", "New account")
|
||||
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
||||
disableSignup := getData(ctx, "DisableSignup")
|
||||
disableForm := getData(ctx, "DisableLoginForm")
|
||||
|
||||
code := ctx.QueryParam("code")
|
||||
if code != "" {
|
||||
if invitation, err := db.GetInvitationByCode(code); err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot check for invitation code", err)
|
||||
} else if invitation != nil && invitation.IsUsable() {
|
||||
disableSignup = false
|
||||
}
|
||||
}
|
||||
|
||||
setData(ctx, "title", trH(ctx, "auth.new-account"))
|
||||
setData(ctx, "htmlTitle", trH(ctx, "auth.new-account"))
|
||||
setData(ctx, "disableForm", disableForm)
|
||||
setData(ctx, "disableSignup", disableSignup)
|
||||
setData(ctx, "isLoginPage", false)
|
||||
return html(ctx, "auth_form.html")
|
||||
}
|
||||
|
||||
func processRegister(ctx echo.Context) error {
|
||||
if getData(ctx, "DisableSignup") == true {
|
||||
return errorRes(403, "Signing up is disabled", nil)
|
||||
disableSignup := getData(ctx, "DisableSignup")
|
||||
|
||||
code := ctx.QueryParam("code")
|
||||
invitation, err := db.GetInvitationByCode(code)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot check for invitation code", err)
|
||||
} else if invitation.ID != 0 && invitation.IsUsable() {
|
||||
disableSignup = false
|
||||
}
|
||||
|
||||
if disableSignup == true {
|
||||
return errorRes(403, tr(ctx, "error.signup-disabled"), nil)
|
||||
}
|
||||
|
||||
if getData(ctx, "DisableLoginForm") == true {
|
||||
return errorRes(403, "Signing up via registration form is disabled", nil)
|
||||
return errorRes(403, tr(ctx, "error.signup-disabled-form"), nil)
|
||||
}
|
||||
|
||||
setData(ctx, "title", "New account")
|
||||
setData(ctx, "htmlTitle", "New account")
|
||||
setData(ctx, "title", trH(ctx, "auth.new-account"))
|
||||
setData(ctx, "htmlTitle", trH(ctx, "auth.new-account"))
|
||||
|
||||
sess := getSession(ctx)
|
||||
|
||||
dto := new(models.UserDTO)
|
||||
dto := new(db.UserDTO)
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, "Cannot bind data", err)
|
||||
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
|
||||
}
|
||||
|
||||
if err := ctx.Validate(dto); err != nil {
|
||||
addFlash(ctx, validationMessages(&err), "error")
|
||||
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
|
||||
return html(ctx, "auth_form.html")
|
||||
}
|
||||
|
||||
if exists, err := models.UserExists(dto.Username); err != nil || exists {
|
||||
addFlash(ctx, "Username already exists", "error")
|
||||
if exists, err := db.UserExists(dto.Username); err != nil || exists {
|
||||
addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error")
|
||||
return html(ctx, "auth_form.html")
|
||||
}
|
||||
|
||||
user := dto.ToUser()
|
||||
|
||||
password, err := argon2id.hash(user.Password)
|
||||
password, err := utils.Argon2id.Hash(user.Password)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot hash password", err)
|
||||
}
|
||||
@@ -80,6 +114,12 @@ func processRegister(ctx echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if invitation.ID != 0 {
|
||||
if err := invitation.Use(); err != nil {
|
||||
return errorRes(500, "Cannot use invitation", err)
|
||||
}
|
||||
}
|
||||
|
||||
sess.Values["user"] = user.ID
|
||||
saveSession(sess, ctx)
|
||||
|
||||
@@ -87,47 +127,49 @@ func processRegister(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func login(ctx echo.Context) error {
|
||||
setData(ctx, "title", "Login")
|
||||
setData(ctx, "htmlTitle", "Login")
|
||||
setData(ctx, "title", trH(ctx, "auth.login"))
|
||||
setData(ctx, "htmlTitle", trH(ctx, "auth.login"))
|
||||
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
||||
setData(ctx, "isLoginPage", true)
|
||||
return html(ctx, "auth_form.html")
|
||||
}
|
||||
|
||||
func processLogin(ctx echo.Context) error {
|
||||
if getData(ctx, "DisableLoginForm") == true {
|
||||
return errorRes(403, "Logging in via login form is disabled", nil)
|
||||
return errorRes(403, tr(ctx, "error.login-disabled-form"), nil)
|
||||
}
|
||||
|
||||
var err error
|
||||
sess := getSession(ctx)
|
||||
|
||||
dto := &models.UserDTO{}
|
||||
dto := &db.UserDTO{}
|
||||
if err = ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, "Cannot bind data", err)
|
||||
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
|
||||
}
|
||||
password := dto.Password
|
||||
|
||||
var user *models.User
|
||||
var user *db.User
|
||||
|
||||
if user, err = models.GetUserByUsername(dto.Username); err != nil {
|
||||
if user, err = db.GetUserByUsername(dto.Username); err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot get user", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
addFlash(ctx, "Invalid credentials", "error")
|
||||
addFlash(ctx, tr(ctx, "flash.auth.invalid-credentials"), "error")
|
||||
return redirect(ctx, "/login")
|
||||
}
|
||||
|
||||
if ok, err := argon2id.verify(password, user.Password); !ok {
|
||||
if ok, err := utils.Argon2id.Verify(password, user.Password); !ok {
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot check for password", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
addFlash(ctx, "Invalid credentials", "error")
|
||||
addFlash(ctx, tr(ctx, "flash.auth.invalid-credentials"), "error")
|
||||
return redirect(ctx, "/login")
|
||||
}
|
||||
|
||||
sess.Values["user"] = user.ID
|
||||
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
|
||||
saveSession(sess, ctx)
|
||||
deleteCsrfCookie(ctx)
|
||||
|
||||
@@ -137,59 +179,45 @@ func processLogin(ctx echo.Context) error {
|
||||
func oauthCallback(ctx echo.Context) error {
|
||||
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
|
||||
if err != nil {
|
||||
return errorRes(400, "Cannot complete user auth", err)
|
||||
return errorRes(400, tr(ctx, "error.complete-oauth-login", err.Error()), err)
|
||||
}
|
||||
|
||||
currUser := getUserLogged(ctx)
|
||||
if currUser != nil {
|
||||
// if user is logged in, link account to user and update its avatar URL
|
||||
switch user.Provider {
|
||||
case "github":
|
||||
currUser.GithubID = user.UserID
|
||||
currUser.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
|
||||
case "gitea":
|
||||
currUser.GiteaID = user.UserID
|
||||
currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
|
||||
}
|
||||
updateUserProviderInfo(currUser, user.Provider, user)
|
||||
|
||||
if err = currUser.Update(); err != nil {
|
||||
return errorRes(500, "Cannot update user "+title.String(user.Provider)+" id", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "Account linked to "+title.String(user.Provider), "success")
|
||||
addFlash(ctx, tr(ctx, "flash.auth.account-linked-oauth", title.String(user.Provider)), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
// if user is not in database, create it
|
||||
userDB, err := models.GetUserByProvider(user.UserID, user.Provider)
|
||||
userDB, err := db.GetUserByProvider(user.UserID, user.Provider)
|
||||
if err != nil {
|
||||
if getData(ctx, "DisableSignup") == true {
|
||||
return errorRes(403, "Signing up is disabled", nil)
|
||||
return errorRes(403, tr(ctx, "error.signup-disabled"), nil)
|
||||
}
|
||||
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot get user", err)
|
||||
}
|
||||
|
||||
userDB = &models.User{
|
||||
userDB = &db.User{
|
||||
Username: user.NickName,
|
||||
Email: user.Email,
|
||||
MD5Hash: fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(user.Email))))),
|
||||
}
|
||||
|
||||
// set provider id and avatar URL
|
||||
switch user.Provider {
|
||||
case "github":
|
||||
userDB.GithubID = user.UserID
|
||||
userDB.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
|
||||
case "gitea":
|
||||
userDB.GiteaID = user.UserID
|
||||
userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
|
||||
}
|
||||
updateUserProviderInfo(userDB, user.Provider, user)
|
||||
|
||||
if err = userDB.Create(); err != nil {
|
||||
if models.IsUniqueConstraintViolation(err) {
|
||||
addFlash(ctx, "Username "+user.NickName+" already exists in Opengist", "error")
|
||||
if db.IsUniqueConstraintViolation(err) {
|
||||
addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error")
|
||||
return redirect(ctx, "/login")
|
||||
}
|
||||
|
||||
@@ -204,10 +232,14 @@ func oauthCallback(ctx echo.Context) error {
|
||||
|
||||
var resp *http.Response
|
||||
switch user.Provider {
|
||||
case "github":
|
||||
case GitHubProvider:
|
||||
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
|
||||
case "gitea":
|
||||
case GitLabProvider:
|
||||
resp, err = http.Get(urlJoin(config.C.GitlabUrl, user.NickName+".keys"))
|
||||
case GiteaProvider:
|
||||
resp, err = http.Get(urlJoin(config.C.GiteaUrl, user.NickName+".keys"))
|
||||
case OpenIDConnect:
|
||||
err = errors.New("cannot get keys from OIDC provider")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
@@ -215,7 +247,7 @@ func oauthCallback(ctx echo.Context) error {
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
addFlash(ctx, "Could not get user keys", "error")
|
||||
addFlash(ctx, tr(ctx, "flash.auth.user-sshkeys-not-retrievable"), "error")
|
||||
log.Error().Err(err).Msg("Could not get user keys")
|
||||
}
|
||||
|
||||
@@ -224,14 +256,14 @@ func oauthCallback(ctx echo.Context) error {
|
||||
keys = keys[:len(keys)-1]
|
||||
}
|
||||
for _, key := range keys {
|
||||
sshKey := models.SSHKey{
|
||||
sshKey := db.SSHKey{
|
||||
Title: "Added from " + user.Provider,
|
||||
Content: key,
|
||||
User: *userDB,
|
||||
}
|
||||
|
||||
if err = sshKey.Create(); err != nil {
|
||||
addFlash(ctx, "Could not create ssh key", "error")
|
||||
addFlash(ctx, tr(ctx, "flash.auth.user-sshkeys-not-created"), "error")
|
||||
log.Error().Err(err).Msg("Could not create ssh key")
|
||||
}
|
||||
}
|
||||
@@ -262,7 +294,7 @@ func oauth(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
switch provider {
|
||||
case "github":
|
||||
case GitHubProvider:
|
||||
goth.UseProviders(
|
||||
github.New(
|
||||
config.C.GithubClientKey,
|
||||
@@ -271,7 +303,19 @@ func oauth(ctx echo.Context) error {
|
||||
),
|
||||
)
|
||||
|
||||
case "gitea":
|
||||
case GitLabProvider:
|
||||
goth.UseProviders(
|
||||
gitlab.NewCustomisedURL(
|
||||
config.C.GitlabClientKey,
|
||||
config.C.GitlabSecret,
|
||||
urlJoin(opengistUrl, "/oauth/gitlab/callback"),
|
||||
urlJoin(config.C.GitlabUrl, "/oauth/authorize"),
|
||||
urlJoin(config.C.GitlabUrl, "/oauth/token"),
|
||||
urlJoin(config.C.GitlabUrl, "/api/v4/user"),
|
||||
),
|
||||
)
|
||||
|
||||
case GiteaProvider:
|
||||
goth.UseProviders(
|
||||
gitea.NewCustomisedURL(
|
||||
config.C.GiteaClientKey,
|
||||
@@ -282,39 +326,50 @@ func oauth(ctx echo.Context) error {
|
||||
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
|
||||
),
|
||||
)
|
||||
case OpenIDConnect:
|
||||
oidcProvider, err := openidConnect.New(
|
||||
config.C.OIDCClientKey,
|
||||
config.C.OIDCSecret,
|
||||
urlJoin(opengistUrl, "/oauth/openid-connect/callback"),
|
||||
config.C.OIDCDiscoveryUrl,
|
||||
"openid",
|
||||
"email",
|
||||
"profile",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot create OIDC provider", err)
|
||||
}
|
||||
|
||||
goth.UseProviders(oidcProvider)
|
||||
}
|
||||
|
||||
currUser := getUserLogged(ctx)
|
||||
if currUser != nil {
|
||||
isDelete := false
|
||||
var err error
|
||||
switch provider {
|
||||
case "github":
|
||||
if currUser.GithubID != "" {
|
||||
isDelete = true
|
||||
err = currUser.DeleteProviderID(provider)
|
||||
}
|
||||
case "gitea":
|
||||
if currUser.GiteaID != "" {
|
||||
isDelete = true
|
||||
err = currUser.DeleteProviderID(provider)
|
||||
}
|
||||
// Map each provider to a function that checks the relevant ID in currUser
|
||||
providerIDCheckMap := map[string]func() bool{
|
||||
GitHubProvider: func() bool { return currUser.GithubID != "" },
|
||||
GitLabProvider: func() bool { return currUser.GitlabID != "" },
|
||||
GiteaProvider: func() bool { return currUser.GiteaID != "" },
|
||||
OpenIDConnect: func() bool { return currUser.OIDCID != "" },
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
|
||||
}
|
||||
// Check if the provider is valid and if the user has a linked ID
|
||||
// Means that the user wants to unlink the account
|
||||
if checkFunc, exists := providerIDCheckMap[provider]; exists && checkFunc() {
|
||||
if err := currUser.DeleteProviderID(provider); err != nil {
|
||||
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
|
||||
}
|
||||
|
||||
if isDelete {
|
||||
addFlash(ctx, "Account unlinked from "+title.String(provider), "success")
|
||||
addFlash(ctx, tr(ctx, "flash.auth.account-unlinked-oauth", title.String(provider)), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
}
|
||||
|
||||
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
|
||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||
if provider != "github" && provider != "gitea" {
|
||||
return errorRes(400, "Unsupported provider", nil)
|
||||
if provider != GitHubProvider && provider != GitLabProvider && provider != GiteaProvider && provider != OpenIDConnect {
|
||||
return errorRes(400, tr(ctx, "error.oauth-unsupported"), nil)
|
||||
}
|
||||
|
||||
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
|
||||
@@ -336,11 +391,28 @@ func urlJoin(base string, elem ...string) string {
|
||||
return joined
|
||||
}
|
||||
|
||||
func updateUserProviderInfo(userDB *db.User, provider string, user goth.User) {
|
||||
userDB.AvatarURL = getAvatarUrlFromProvider(provider, user.UserID)
|
||||
switch provider {
|
||||
case GitHubProvider:
|
||||
userDB.GithubID = user.UserID
|
||||
case GitLabProvider:
|
||||
userDB.GitlabID = user.UserID
|
||||
case GiteaProvider:
|
||||
userDB.GiteaID = user.UserID
|
||||
case OpenIDConnect:
|
||||
userDB.OIDCID = user.UserID
|
||||
userDB.AvatarURL = user.AvatarURL
|
||||
}
|
||||
}
|
||||
|
||||
func getAvatarUrlFromProvider(provider string, identifier string) string {
|
||||
switch provider {
|
||||
case "github":
|
||||
case GitHubProvider:
|
||||
return "https://avatars.githubusercontent.com/u/" + identifier + "?v=4"
|
||||
case "gitea":
|
||||
case GitLabProvider:
|
||||
return urlJoin(config.C.GitlabUrl, "/uploads/-/system/user/avatar/", identifier, "/avatar.png") + "?width=400"
|
||||
case GiteaProvider:
|
||||
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", identifier))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get user from Gitea")
|
||||
@@ -370,3 +442,15 @@ func getAvatarUrlFromProvider(provider string, identifier string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ContextAuthInfo struct {
|
||||
context echo.Context
|
||||
}
|
||||
|
||||
func (auth ContextAuthInfo) RequireLogin() (bool, error) {
|
||||
return getData(auth.context, "RequireLogin") == true, nil
|
||||
}
|
||||
|
||||
func (auth ContextAuthInfo) AllowGistsWithoutLogin() (bool, error) {
|
||||
return getData(auth.context, "AllowGistsWithoutLogin") == true, nil
|
||||
}
|
||||
|
||||
@@ -2,31 +2,62 @@ package web
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"gorm.io/gorm"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"github.com/thomiceli/opengist/internal/render"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
currUser := getUserLogged(ctx)
|
||||
|
||||
userName := ctx.Param("user")
|
||||
gistName := ctx.Param("gistname")
|
||||
|
||||
gistName = strings.TrimSuffix(gistName, ".git")
|
||||
switch filepath.Ext(gistName) {
|
||||
case ".js":
|
||||
setData(ctx, "gistpage", "js")
|
||||
gistName = strings.TrimSuffix(gistName, ".js")
|
||||
case ".json":
|
||||
setData(ctx, "gistpage", "json")
|
||||
gistName = strings.TrimSuffix(gistName, ".json")
|
||||
case ".git":
|
||||
setData(ctx, "gistpage", "git")
|
||||
gistName = strings.TrimSuffix(gistName, ".git")
|
||||
}
|
||||
|
||||
gist, err := models.GetGist(userName, gistName)
|
||||
gist, err := db.GetGist(userName, gistName)
|
||||
if err != nil {
|
||||
return notFound("Gist not found")
|
||||
}
|
||||
|
||||
if gist.Private == db.PrivateVisibility {
|
||||
if currUser == nil || currUser.ID != gist.UserID {
|
||||
return notFound("Gist not found")
|
||||
}
|
||||
}
|
||||
|
||||
setData(ctx, "gist", gist)
|
||||
|
||||
if config.C.SshGit {
|
||||
@@ -45,19 +76,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
httpProtocol := "http"
|
||||
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
|
||||
httpProtocol = "https"
|
||||
}
|
||||
setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))
|
||||
|
||||
var baseHttpUrl string
|
||||
// if a custom external url is set, use it
|
||||
if config.C.ExternalUrl != "" {
|
||||
baseHttpUrl = config.C.ExternalUrl
|
||||
} else {
|
||||
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
|
||||
}
|
||||
baseHttpUrl := getData(ctx, "baseHttpUrl").(string)
|
||||
|
||||
if config.C.HttpGit {
|
||||
setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
|
||||
@@ -65,6 +84,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
|
||||
setData(ctx, "httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
|
||||
setData(ctx, "currentUrl", template.URL(ctx.Request().URL.Path))
|
||||
setData(ctx, "embedScript", fmt.Sprintf(`<script src="%s"></script>`, baseHttpUrl+"/"+userName+"/"+gistName+".js"))
|
||||
|
||||
nbCommits, err := gist.NbCommits()
|
||||
if err != nil {
|
||||
@@ -72,7 +92,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
}
|
||||
setData(ctx, "nbCommits", nbCommits)
|
||||
|
||||
if currUser := getUserLogged(ctx); currUser != nil {
|
||||
if currUser != nil {
|
||||
hasLiked, err := currUser.HasLiked(gist)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot get user like status", err)
|
||||
@@ -80,7 +100,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
setData(ctx, "hasLiked", hasLiked)
|
||||
}
|
||||
|
||||
if gist.Private {
|
||||
if gist.Private > 0 {
|
||||
setData(ctx, "NoIndex", true)
|
||||
}
|
||||
|
||||
@@ -88,6 +108,30 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// gistSoftInit try to load a gist (same as gistInit) but does not return a 404 if the gist is not found
|
||||
// useful for git clients using HTTP to obfuscate the existence of a private gist
|
||||
func gistSoftInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
userName := ctx.Param("user")
|
||||
gistName := ctx.Param("gistname")
|
||||
|
||||
gistName = strings.TrimSuffix(gistName, ".git")
|
||||
|
||||
gist, _ := db.GetGist(userName, gistName)
|
||||
setData(ctx, "gist", gist)
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// gistNewPushInit has the same behavior as gistSoftInit but create a new gist empty instead
|
||||
func gistNewPushSoftInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
setData(c, "gist", new(db.Gist))
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func allGists(ctx echo.Context) error {
|
||||
var err error
|
||||
var urlPage string
|
||||
@@ -97,22 +141,24 @@ func allGists(ctx echo.Context) error {
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
sort := "created"
|
||||
sortText := trH(ctx, "gist.list.sort-by-created")
|
||||
order := "desc"
|
||||
orderText := "Recently"
|
||||
orderText := trH(ctx, "gist.list.order-by-desc")
|
||||
|
||||
if ctx.QueryParam("sort") == "updated" {
|
||||
sort = "updated"
|
||||
sortText = trH(ctx, "gist.list.sort-by-updated")
|
||||
}
|
||||
|
||||
if ctx.QueryParam("order") == "asc" {
|
||||
order = "asc"
|
||||
orderText = "Least recently"
|
||||
orderText = trH(ctx, "gist.list.order-by-asc")
|
||||
}
|
||||
|
||||
setData(ctx, "sort", sort)
|
||||
setData(ctx, "sort", sortText)
|
||||
setData(ctx, "order", orderText)
|
||||
|
||||
var gists []*models.Gist
|
||||
var gists []*db.Gist
|
||||
var currentUserId uint
|
||||
if userLogged != nil {
|
||||
currentUserId = userLogged.ID
|
||||
@@ -123,17 +169,17 @@ func allGists(ctx echo.Context) error {
|
||||
if fromUserStr == "" {
|
||||
urlctx := ctx.Request().URL.Path
|
||||
if strings.HasSuffix(urlctx, "search") {
|
||||
setData(ctx, "htmlTitle", "Search results")
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.list.search-results"))
|
||||
setData(ctx, "mode", "search")
|
||||
setData(ctx, "searchQuery", ctx.QueryParam("q"))
|
||||
setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
|
||||
urlPage = "search"
|
||||
gists, err = models.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
|
||||
gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
|
||||
} else if strings.HasSuffix(urlctx, "all") {
|
||||
setData(ctx, "htmlTitle", "All gists")
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all"))
|
||||
setData(ctx, "mode", "all")
|
||||
urlPage = "all"
|
||||
gists, err = models.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
|
||||
gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
|
||||
}
|
||||
} else {
|
||||
liked := false
|
||||
@@ -149,9 +195,9 @@ func allGists(ctx echo.Context) error {
|
||||
return errorRes(500, "Error matching regexp", err)
|
||||
}
|
||||
|
||||
var fromUser *models.User
|
||||
var fromUser *db.User
|
||||
|
||||
fromUser, err = models.GetUserByUsername(fromUserStr)
|
||||
fromUser, err = db.GetUserByUsername(fromUserStr)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return notFound("User not found")
|
||||
@@ -160,19 +206,19 @@ func allGists(ctx echo.Context) error {
|
||||
}
|
||||
setData(ctx, "fromUser", fromUser)
|
||||
|
||||
if countFromUser, err := models.CountAllGistsFromUser(fromUser.ID, currentUserId); err != nil {
|
||||
if countFromUser, err := db.CountAllGistsFromUser(fromUser.ID, currentUserId); err != nil {
|
||||
return errorRes(500, "Error counting gists", err)
|
||||
} else {
|
||||
setData(ctx, "countFromUser", countFromUser)
|
||||
}
|
||||
|
||||
if countLiked, err := models.CountAllGistsLikedByUser(fromUser.ID, currentUserId); err != nil {
|
||||
if countLiked, err := db.CountAllGistsLikedByUser(fromUser.ID, currentUserId); err != nil {
|
||||
return errorRes(500, "Error counting liked gists", err)
|
||||
} else {
|
||||
setData(ctx, "countLiked", countLiked)
|
||||
}
|
||||
|
||||
if countForked, err := models.CountAllGistsForkedByUser(fromUser.ID, currentUserId); err != nil {
|
||||
if countForked, err := db.CountAllGistsForkedByUser(fromUser.ID, currentUserId); err != nil {
|
||||
return errorRes(500, "Error counting forked gists", err)
|
||||
} else {
|
||||
setData(ctx, "countForked", countForked)
|
||||
@@ -180,63 +226,222 @@ func allGists(ctx echo.Context) error {
|
||||
|
||||
if liked {
|
||||
urlPage = fromUserStr + "/liked"
|
||||
setData(ctx, "htmlTitle", "All gists liked by "+fromUserStr)
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-liked-by", fromUserStr))
|
||||
setData(ctx, "mode", "liked")
|
||||
gists, err = models.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||
gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||
} else if forked {
|
||||
urlPage = fromUserStr + "/forked"
|
||||
setData(ctx, "htmlTitle", "All gists forked by "+fromUserStr)
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-forked-by", fromUserStr))
|
||||
setData(ctx, "mode", "forked")
|
||||
gists, err = models.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||
gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||
} else {
|
||||
urlPage = fromUserStr
|
||||
setData(ctx, "htmlTitle", "All gists from "+fromUserStr)
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.list.all-from", fromUserStr))
|
||||
setData(ctx, "mode", "fromUser")
|
||||
gists, err = models.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||
gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
|
||||
}
|
||||
}
|
||||
|
||||
renderedGists := make([]*render.RenderedGist, 0, len(gists))
|
||||
for _, gist := range gists {
|
||||
rendered, err := render.HighlightGistPreview(gist)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error rendering gist preview for " + gist.Identifier() + " - " + gist.PreviewFilename)
|
||||
}
|
||||
renderedGists = append(renderedGists, &rendered)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching gists", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, gists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
if err = paginate(ctx, renderedGists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
|
||||
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
|
||||
}
|
||||
|
||||
setData(ctx, "urlPage", urlPage)
|
||||
return html(ctx, "all.html")
|
||||
}
|
||||
|
||||
func search(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
content, meta := parseSearchQueryStr(ctx.QueryParam("q"))
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
var currentUserId uint
|
||||
userLogged := getUserLogged(ctx)
|
||||
if userLogged != nil {
|
||||
currentUserId = userLogged.ID
|
||||
} else {
|
||||
currentUserId = 0
|
||||
}
|
||||
|
||||
var visibleGistsIds []uint
|
||||
visibleGistsIds, err = db.GetAllGistsVisibleByUser(currentUserId)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching gists", err)
|
||||
}
|
||||
|
||||
gistsIds, nbHits, langs, err := index.SearchGists(content, index.SearchGistMetadata{
|
||||
Username: meta["user"],
|
||||
Title: meta["title"],
|
||||
Filename: meta["filename"],
|
||||
Extension: meta["extension"],
|
||||
Language: meta["language"],
|
||||
}, visibleGistsIds, pageInt)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error searching gists", err)
|
||||
}
|
||||
|
||||
gists, err := db.GetAllGistsByIds(gistsIds)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching gists", err)
|
||||
}
|
||||
|
||||
renderedGists := make([]*render.RenderedGist, 0, len(gists))
|
||||
for _, gist := range gists {
|
||||
rendered, err := render.HighlightGistPreview(gist)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error rendering gist preview for " + gist.Identifier() + " - " + gist.PreviewFilename)
|
||||
}
|
||||
renderedGists = append(renderedGists, &rendered)
|
||||
}
|
||||
|
||||
if pageInt > 1 && len(renderedGists) != 0 {
|
||||
setData(ctx, "prevPage", pageInt-1)
|
||||
}
|
||||
if 10*pageInt < int(nbHits) {
|
||||
setData(ctx, "nextPage", pageInt+1)
|
||||
}
|
||||
setData(ctx, "prevLabel", trH(ctx, "pagination.previous"))
|
||||
setData(ctx, "nextLabel", trH(ctx, "pagination.next"))
|
||||
setData(ctx, "urlPage", "search")
|
||||
setData(ctx, "urlParams", template.URL("&q="+ctx.QueryParam("q")))
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.list.search-results"))
|
||||
setData(ctx, "nbHits", nbHits)
|
||||
setData(ctx, "gists", renderedGists)
|
||||
setData(ctx, "langs", langs)
|
||||
setData(ctx, "searchQuery", ctx.QueryParam("q"))
|
||||
return html(ctx, "search.html")
|
||||
}
|
||||
|
||||
func gistIndex(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
if getData(ctx, "gistpage") == "js" {
|
||||
return gistJs(ctx)
|
||||
} else if getData(ctx, "gistpage") == "json" {
|
||||
return gistJson(ctx)
|
||||
}
|
||||
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
revision := ctx.Param("revision")
|
||||
|
||||
if revision == "" {
|
||||
revision = "HEAD"
|
||||
}
|
||||
|
||||
files, err := gist.Files(revision)
|
||||
if err != nil {
|
||||
files, err := gist.Files(revision, true)
|
||||
if _, ok := err.(*git.RevisionNotFoundError); ok {
|
||||
return notFound("Revision not found")
|
||||
} else if err != nil {
|
||||
return errorRes(500, "Error fetching files", err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return notFound("Revision not found")
|
||||
}
|
||||
renderedFiles := render.HighlightFiles(files)
|
||||
|
||||
setData(ctx, "page", "code")
|
||||
setData(ctx, "commit", revision)
|
||||
setData(ctx, "files", files)
|
||||
setData(ctx, "files", renderedFiles)
|
||||
setData(ctx, "revision", revision)
|
||||
setData(ctx, "htmlTitle", gist.Title)
|
||||
return html(ctx, "gist.html")
|
||||
}
|
||||
|
||||
func gistJson(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
files, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching files", err)
|
||||
}
|
||||
|
||||
renderedFiles := render.HighlightFiles(files)
|
||||
setData(ctx, "files", renderedFiles)
|
||||
|
||||
htmlbuf := bytes.Buffer{}
|
||||
w := bufio.NewWriter(&htmlbuf)
|
||||
if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", dataMap(ctx), ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
jsUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), gist.User.Username, gist.Identifier()+".js")
|
||||
if err != nil {
|
||||
return errorRes(500, "Error joining js url", err)
|
||||
}
|
||||
|
||||
cssUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), manifestEntries["embed.css"].File)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error joining css url", err)
|
||||
}
|
||||
|
||||
return ctx.JSON(200, map[string]interface{}{
|
||||
"owner": gist.User.Username,
|
||||
"id": gist.Identifier(),
|
||||
"uuid": gist.Uuid,
|
||||
"title": gist.Title,
|
||||
"description": gist.Description,
|
||||
"created_at": time.Unix(gist.CreatedAt, 0).Format(time.RFC3339),
|
||||
"visibility": gist.VisibilityStr(),
|
||||
"files": renderedFiles,
|
||||
"embed": map[string]string{
|
||||
"html": htmlbuf.String(),
|
||||
"css": cssUrl,
|
||||
"js": jsUrl,
|
||||
"js_dark": jsUrl + "?dark",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func gistJs(ctx echo.Context) error {
|
||||
if _, exists := ctx.QueryParams()["dark"]; exists {
|
||||
setData(ctx, "dark", "dark")
|
||||
}
|
||||
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
files, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching files", err)
|
||||
}
|
||||
|
||||
renderedFiles := render.HighlightFiles(files)
|
||||
setData(ctx, "files", renderedFiles)
|
||||
|
||||
htmlbuf := bytes.Buffer{}
|
||||
w := bufio.NewWriter(&htmlbuf)
|
||||
if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", dataMap(ctx), ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
cssUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), manifestEntries["embed.css"].File)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error joining css url", err)
|
||||
}
|
||||
|
||||
js := `document.write('<link rel="stylesheet" href="%s">')
|
||||
document.write('%s')
|
||||
`
|
||||
content := strings.Replace(htmlbuf.String(), `\n`, `\\n`, -1)
|
||||
content = strings.Replace(content, "\n", `\n`, -1)
|
||||
js = fmt.Sprintf(js, cssUrl, content)
|
||||
ctx.Response().Header().Set("Content-Type", "application/javascript")
|
||||
return plainText(ctx, 200, js)
|
||||
}
|
||||
|
||||
func revisions(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
userName := gist.User.Username
|
||||
gistName := gist.Uuid
|
||||
gistName := gist.Identifier()
|
||||
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
@@ -246,7 +451,7 @@ func revisions(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
|
||||
}
|
||||
|
||||
emailsSet := map[string]struct{}{}
|
||||
@@ -257,7 +462,7 @@ func revisions(ctx echo.Context) error {
|
||||
emailsSet[strings.ToLower(commit.AuthorEmail)] = struct{}{}
|
||||
}
|
||||
|
||||
emailsUsers, err := models.GetUsersFromEmails(emailsSet)
|
||||
emailsUsers, err := db.GetUsersFromEmails(emailsSet)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching users emails", err)
|
||||
}
|
||||
@@ -265,13 +470,13 @@ func revisions(ctx echo.Context) error {
|
||||
setData(ctx, "page", "revisions")
|
||||
setData(ctx, "revision", "HEAD")
|
||||
setData(ctx, "emails", emailsUsers)
|
||||
setData(ctx, "htmlTitle", "Revision of "+gist.Title)
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.revision-of", gist.Title))
|
||||
|
||||
return html(ctx, "revisions.html")
|
||||
}
|
||||
|
||||
func create(ctx echo.Context) error {
|
||||
setData(ctx, "htmlTitle", "Create a new gist")
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist"))
|
||||
return html(ctx, "create.html")
|
||||
}
|
||||
|
||||
@@ -283,24 +488,24 @@ func processCreate(ctx echo.Context) error {
|
||||
|
||||
err := ctx.Request().ParseForm()
|
||||
if err != nil {
|
||||
return errorRes(400, "Bad request", err)
|
||||
return errorRes(400, tr(ctx, "error.bad-request"), err)
|
||||
}
|
||||
|
||||
dto := new(models.GistDTO)
|
||||
var gist *models.Gist
|
||||
dto := new(db.GistDTO)
|
||||
var gist *db.Gist
|
||||
|
||||
if isCreate {
|
||||
setData(ctx, "htmlTitle", "Create a new gist")
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.new.create-a-new-gist"))
|
||||
} else {
|
||||
gist = getData(ctx, "gist").(*models.Gist)
|
||||
setData(ctx, "htmlTitle", "Edit "+gist.Title)
|
||||
gist = getData(ctx, "gist").(*db.Gist)
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.edit.edit-gist", gist.Title))
|
||||
}
|
||||
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, "Cannot bind data", err)
|
||||
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
|
||||
}
|
||||
|
||||
dto.Files = make([]models.FileDTO, 0)
|
||||
dto.Files = make([]db.FileDTO, 0)
|
||||
fileCounter := 0
|
||||
for i := 0; i < len(ctx.Request().PostForm["content"]); i++ {
|
||||
name := ctx.Request().PostForm["name"][i]
|
||||
@@ -313,10 +518,10 @@ func processCreate(ctx echo.Context) error {
|
||||
|
||||
escapedValue, err := url.QueryUnescape(content)
|
||||
if err != nil {
|
||||
return errorRes(400, "Invalid character unescaped", err)
|
||||
return errorRes(400, tr(ctx, "error.invalid-character-unescaped"), err)
|
||||
}
|
||||
|
||||
dto.Files = append(dto.Files, models.FileDTO{
|
||||
dto.Files = append(dto.Files, db.FileDTO{
|
||||
Filename: strings.Trim(name, " "),
|
||||
Content: escapedValue,
|
||||
})
|
||||
@@ -324,11 +529,11 @@ func processCreate(ctx echo.Context) error {
|
||||
|
||||
err = ctx.Validate(dto)
|
||||
if err != nil {
|
||||
addFlash(ctx, validationMessages(&err), "error")
|
||||
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
|
||||
if isCreate {
|
||||
return html(ctx, "create.html")
|
||||
} else {
|
||||
files, err := gist.Files("HEAD")
|
||||
files, err := gist.Files("HEAD", false)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching files", err)
|
||||
}
|
||||
@@ -394,39 +599,42 @@ func processCreate(ctx echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
return redirect(ctx, "/"+user.Username+"/"+gist.Uuid)
|
||||
gist.AddInIndex()
|
||||
|
||||
return redirect(ctx, "/"+user.Username+"/"+gist.Identifier())
|
||||
}
|
||||
|
||||
func toggleVisibility(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
func editVisibility(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
gist.Private = !gist.Private
|
||||
if err := gist.Update(); err != nil {
|
||||
dto := new(db.VisibilityDTO)
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
|
||||
}
|
||||
|
||||
gist.Private = dto.Private
|
||||
if err := gist.UpdateNoTimestamps(); err != nil {
|
||||
return errorRes(500, "Error updating this gist", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "Gist visibility has been changed", "success")
|
||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid)
|
||||
addFlash(ctx, tr(ctx, "flash.gist.visibility-changed"), "success")
|
||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
|
||||
}
|
||||
|
||||
func deleteGist(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
|
||||
err := gist.DeleteRepository()
|
||||
if err != nil {
|
||||
return errorRes(500, "Error deleting the repository", err)
|
||||
}
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
if err := gist.Delete(); err != nil {
|
||||
return errorRes(500, "Error deleting this gist", err)
|
||||
}
|
||||
gist.RemoveFromIndex()
|
||||
|
||||
addFlash(ctx, "Gist has been deleted", "success")
|
||||
addFlash(ctx, tr(ctx, "flash.gist.deleted"), "success")
|
||||
return redirect(ctx, "/")
|
||||
}
|
||||
|
||||
func like(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
currentUser := getUserLogged(ctx)
|
||||
|
||||
hasLiked, err := currentUser.HasLiked(gist)
|
||||
@@ -444,7 +652,7 @@ func like(ctx echo.Context) error {
|
||||
return errorRes(500, "Error liking/dislking this gist", err)
|
||||
}
|
||||
|
||||
redirectTo := "/" + gist.User.Username + "/" + gist.Uuid
|
||||
redirectTo := "/" + gist.User.Username + "/" + gist.Identifier()
|
||||
if r := ctx.QueryParam("redirecturl"); r != "" {
|
||||
redirectTo = r
|
||||
}
|
||||
@@ -452,7 +660,7 @@ func like(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func fork(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
currentUser := getUserLogged(ctx)
|
||||
|
||||
alreadyForked, err := gist.GetForkParent(currentUser)
|
||||
@@ -461,12 +669,12 @@ func fork(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
if gist.User.ID == currentUser.ID {
|
||||
addFlash(ctx, "Unable to fork own gists", "error")
|
||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid)
|
||||
addFlash(ctx, tr(ctx, "flash.gist.fork-own-gist"), "error")
|
||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
|
||||
}
|
||||
|
||||
if alreadyForked.ID != 0 {
|
||||
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Uuid)
|
||||
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Identifier())
|
||||
}
|
||||
|
||||
uuidGist, err := uuid.NewRandom()
|
||||
@@ -474,7 +682,7 @@ func fork(ctx echo.Context) error {
|
||||
return errorRes(500, "Error creating an UUID", err)
|
||||
}
|
||||
|
||||
newGist := &models.Gist{
|
||||
newGist := &db.Gist{
|
||||
Uuid: strings.Replace(uuidGist.String(), "-", "", -1),
|
||||
Title: gist.Title,
|
||||
Preview: gist.Preview,
|
||||
@@ -497,15 +705,14 @@ func fork(ctx echo.Context) error {
|
||||
return errorRes(500, "Error incrementing the fork count", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "Gist has been forked", "success")
|
||||
addFlash(ctx, tr(ctx, "flash.gist.forked"), "success")
|
||||
|
||||
return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Uuid)
|
||||
return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Identifier())
|
||||
}
|
||||
|
||||
func rawFile(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Error getting file content", err)
|
||||
}
|
||||
@@ -517,25 +724,47 @@ func rawFile(ctx echo.Context) error {
|
||||
return plainText(ctx, 200, file.Content)
|
||||
}
|
||||
|
||||
func edit(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
func downloadFile(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error getting file content", err)
|
||||
}
|
||||
|
||||
files, err := gist.Files("HEAD")
|
||||
if file == nil {
|
||||
return notFound("File not found")
|
||||
}
|
||||
|
||||
ctx.Response().Header().Set("Content-Type", "text/plain")
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
|
||||
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
|
||||
_, err = ctx.Response().Write([]byte(file.Content))
|
||||
if err != nil {
|
||||
return errorRes(500, "Error downloading the file", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func edit(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
files, err := gist.Files("HEAD", false)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching files from repository", err)
|
||||
}
|
||||
|
||||
setData(ctx, "files", files)
|
||||
setData(ctx, "htmlTitle", "Edit "+gist.Title)
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.edit.edit-gist", gist.Title))
|
||||
|
||||
return html(ctx, "edit.html")
|
||||
}
|
||||
|
||||
func downloadZip(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
var revision = ctx.Param("revision")
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
revision := ctx.Param("revision")
|
||||
|
||||
files, err := gist.Files(revision)
|
||||
files, err := gist.Files(revision, false)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error fetching files from repository", err)
|
||||
}
|
||||
@@ -567,7 +796,7 @@ func downloadZip(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
ctx.Response().Header().Set("Content-Type", "application/zip")
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Uuid+".zip")
|
||||
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Identifier()+".zip")
|
||||
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(zipFile.Bytes())))
|
||||
_, err = ctx.Response().Write(zipFile.Bytes())
|
||||
if err != nil {
|
||||
@@ -577,7 +806,7 @@ func downloadZip(ctx echo.Context) error {
|
||||
}
|
||||
|
||||
func likes(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
@@ -586,17 +815,17 @@ func likes(ctx echo.Context) error {
|
||||
return errorRes(500, "Error getting users who liked this gist", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Uuid+"/likes", 1); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Identifier()+"/likes", 1); err != nil {
|
||||
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
|
||||
}
|
||||
|
||||
setData(ctx, "htmlTitle", "Like for "+gist.Title)
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.likes.for", gist.Title))
|
||||
setData(ctx, "revision", "HEAD")
|
||||
return html(ctx, "likes.html")
|
||||
}
|
||||
|
||||
func forks(ctx echo.Context) error {
|
||||
var gist = getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
pageInt := getPage(ctx)
|
||||
|
||||
currentUser := getUserLogged(ctx)
|
||||
@@ -610,11 +839,58 @@ func forks(ctx echo.Context) error {
|
||||
return errorRes(500, "Error getting users who liked this gist", err)
|
||||
}
|
||||
|
||||
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Uuid+"/forks", 2); err != nil {
|
||||
return errorRes(404, "Page not found", nil)
|
||||
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Identifier()+"/forks", 2); err != nil {
|
||||
return errorRes(404, tr(ctx, "error.page-not-found"), nil)
|
||||
}
|
||||
|
||||
setData(ctx, "htmlTitle", "Forks for "+gist.Title)
|
||||
setData(ctx, "htmlTitle", trH(ctx, "gist.forks.for", gist.Title))
|
||||
setData(ctx, "revision", "HEAD")
|
||||
return html(ctx, "forks.html")
|
||||
}
|
||||
|
||||
func checkbox(ctx echo.Context) error {
|
||||
filename := ctx.FormValue("file")
|
||||
checkboxNb := ctx.FormValue("checkbox")
|
||||
|
||||
i, err := strconv.Atoi(checkboxNb)
|
||||
if err != nil {
|
||||
return errorRes(400, tr(ctx, "error.invalid-number"), nil)
|
||||
}
|
||||
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
file, err := gist.File("HEAD", filename, false)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error getting file content", err)
|
||||
} else if file == nil {
|
||||
return notFound("File not found")
|
||||
}
|
||||
|
||||
markdown, err := render.Checkbox(file.Content, i)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error checking checkbox", err)
|
||||
}
|
||||
|
||||
if err = gist.AddAndCommitFile(&db.FileDTO{
|
||||
Filename: filename,
|
||||
Content: markdown,
|
||||
}); err != nil {
|
||||
return errorRes(500, "Error adding and committing files", err)
|
||||
}
|
||||
|
||||
if err = gist.UpdatePreviewAndCount(true); err != nil {
|
||||
return errorRes(500, "Error updating the gist", err)
|
||||
}
|
||||
|
||||
return plainText(ctx, 200, "ok")
|
||||
}
|
||||
|
||||
func preview(ctx echo.Context) error {
|
||||
content := ctx.FormValue("content")
|
||||
|
||||
previewStr, err := render.MarkdownString(content)
|
||||
if err != nil {
|
||||
return errorRes(500, "Error rendering markdown", err)
|
||||
}
|
||||
|
||||
return plainText(ctx, 200, previewStr)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,9 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -17,6 +15,15 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/auth"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/memdb"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var routes = []struct {
|
||||
@@ -45,25 +52,36 @@ func gitHttp(ctx echo.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
noAuth := (ctx.QueryParam("service") == "git-upload-pack" ||
|
||||
isInit := strings.HasPrefix(ctx.Request().URL.Path, "/init/info/refs")
|
||||
isInitReceive := strings.HasPrefix(ctx.Request().URL.Path, "/init/git-receive-pack")
|
||||
isInfoRefs := strings.HasSuffix(route.gitUrl, "/info/refs$")
|
||||
isPull := ctx.QueryParam("service") == "git-upload-pack" ||
|
||||
strings.HasSuffix(ctx.Request().URL.Path, "git-upload-pack") ||
|
||||
ctx.Request().Method == "GET") &&
|
||||
!getData(ctx, "RequireLogin").(bool)
|
||||
ctx.Request().Method == "GET" && !isInfoRefs
|
||||
|
||||
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
|
||||
|
||||
if _, err := os.Stat(repositoryPath); os.IsNotExist(err) {
|
||||
if err != nil {
|
||||
return errorRes(500, "Repository does not exist", err)
|
||||
log.Info().Err(err).Msg("Repository directory does not exist")
|
||||
return errorRes(404, "Repository directory does not exist", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Set("repositoryPath", repositoryPath)
|
||||
setData(ctx, "repositoryPath", repositoryPath)
|
||||
|
||||
// Requires Basic Auth if we push the repository
|
||||
if noAuth {
|
||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, true)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Cannot check if unauthenticated access is allowed")
|
||||
}
|
||||
|
||||
// Shows basic auth if :
|
||||
// - user wants to push the gist
|
||||
// - user wants to clone/pull a private gist
|
||||
// - gist is not found (obfuscation)
|
||||
// - admin setting to require login is set to true
|
||||
if isPull && gist.Private != db.PrivateVisibility && gist.ID != 0 && allow {
|
||||
return route.handler(ctx)
|
||||
}
|
||||
|
||||
@@ -82,12 +100,77 @@ func gitHttp(ctx echo.Context) error {
|
||||
return basicAuth(ctx)
|
||||
}
|
||||
|
||||
if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot verify password", err)
|
||||
if !isInit && !isInitReceive {
|
||||
if gist.ID == 0 {
|
||||
return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
|
||||
var userToCheckPermissions *db.User
|
||||
if gist.Private != db.PrivateVisibility && isPull {
|
||||
userToCheckPermissions, _ = db.GetUserByUsername(authUsername)
|
||||
} else {
|
||||
userToCheckPermissions = &gist.User
|
||||
}
|
||||
|
||||
if ok, err := utils.Argon2id.Verify(authPassword, userToCheckPermissions.Password); !ok {
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot verify password", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
} else {
|
||||
var user *db.User
|
||||
if user, err = db.GetUserByUsername(authUsername); err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot get user", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return errorRes(401, "Invalid credentials", nil)
|
||||
}
|
||||
|
||||
if ok, err := utils.Argon2id.Verify(authPassword, user.Password); !ok {
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot check for password", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return errorRes(401, "Invalid credentials", nil)
|
||||
}
|
||||
|
||||
if isInit {
|
||||
gist = new(db.Gist)
|
||||
gist.UserID = user.ID
|
||||
gist.User = *user
|
||||
uuidGist, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return errorRes(500, "Error creating an UUID", err)
|
||||
}
|
||||
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
|
||||
gist.Title = "gist:" + gist.Uuid
|
||||
|
||||
if err = gist.InitRepository(); err != nil {
|
||||
return errorRes(500, "Cannot init repository in the file system", err)
|
||||
}
|
||||
|
||||
if err = gist.Create(); err != nil {
|
||||
return errorRes(500, "Cannot init repository in database", err)
|
||||
}
|
||||
|
||||
if err := memdb.InsertGistInit(user.ID, gist); err != nil {
|
||||
return errorRes(500, "Cannot save the URL for the new Gist", err)
|
||||
}
|
||||
|
||||
setData(ctx, "gist", gist)
|
||||
} else {
|
||||
gistFromMemdb, err := memdb.GetGistInitAndDelete(user.ID)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot get the gist link from the in memory database", err)
|
||||
}
|
||||
|
||||
gist := gistFromMemdb.Gist
|
||||
setData(ctx, "gist", gist)
|
||||
setData(ctx, "repositoryPath", git.RepositoryPath(gist.User.Username, gist.Uuid))
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return errorRes(403, "Unauthorized", nil)
|
||||
}
|
||||
|
||||
return route.handler(ctx)
|
||||
@@ -123,7 +206,8 @@ func pack(ctx echo.Context, serviceType string) error {
|
||||
}
|
||||
}
|
||||
|
||||
repositoryPath := ctx.Get("repositoryPath").(string)
|
||||
repositoryPath := getData(ctx, "repositoryPath").(string)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath)
|
||||
@@ -131,16 +215,14 @@ func pack(ctx echo.Context, serviceType string) error {
|
||||
cmd.Stdin = reqBody
|
||||
cmd.Stdout = ctx.Response().Writer
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "OPENGIST_REPOSITORY_URL_INTERNAL="+git.RepositoryUrl(ctx, gist.User.Username, gist.Identifier()))
|
||||
cmd.Env = append(cmd.Env, "OPENGIST_REPOSITORY_ID="+strconv.Itoa(int(gist.ID)))
|
||||
|
||||
if err = cmd.Run(); err != nil {
|
||||
return errorRes(500, "Cannot run git "+serviceType+" ; "+stderr.String(), err)
|
||||
}
|
||||
|
||||
// updatedAt is updated only if serviceType is receive-pack
|
||||
if serviceType == "receive-pack" {
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
_ = gist.SetLastActiveNow()
|
||||
_ = gist.UpdatePreviewAndCount()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -148,7 +230,7 @@ func infoRefs(ctx echo.Context) error {
|
||||
noCacheHeaders(ctx)
|
||||
var service string
|
||||
|
||||
gist := getData(ctx, "gist").(*models.Gist)
|
||||
gist := getData(ctx, "gist").(*db.Gist)
|
||||
|
||||
serviceType := ctx.QueryParam("service")
|
||||
if strings.HasPrefix(serviceType, "git-") {
|
||||
@@ -232,7 +314,7 @@ func basicAuthDecode(encoded string) (string, string, error) {
|
||||
|
||||
func sendFile(ctx echo.Context, contentType string) error {
|
||||
gitFile := "/" + strings.Join(strings.Split(ctx.Request().URL.Path, "/")[3:], "/")
|
||||
gitFile = path.Join(ctx.Get("repositoryPath").(string), gitFile)
|
||||
gitFile = path.Join(getData(ctx, "repositoryPath").(string), gitFile)
|
||||
fi, err := os.Stat(gitFile)
|
||||
if os.IsNotExist(err) {
|
||||
return errorRes(404, "File not found", nil)
|
||||
|
||||
25
internal/web/healthcheck.go
Normal file
25
internal/web/healthcheck.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"time"
|
||||
)
|
||||
|
||||
func healthcheck(ctx echo.Context) error {
|
||||
// Check database connection
|
||||
dbOk := "ok"
|
||||
httpStatus := 200
|
||||
|
||||
err := db.Ping()
|
||||
if err != nil {
|
||||
dbOk = "ko"
|
||||
httpStatus = 503
|
||||
}
|
||||
|
||||
return ctx.JSON(httpStatus, map[string]interface{}{
|
||||
"opengist": "ok",
|
||||
"database": dbOk,
|
||||
"time": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var dev = os.Getenv("OG_DEV") == "1"
|
||||
var store *sessions.CookieStore
|
||||
var re = regexp.MustCompile("[^a-z0-9]+")
|
||||
var fm = template.FuncMap{
|
||||
"split": strings.Split,
|
||||
"indexByte": strings.IndexByte,
|
||||
"toInt": func(i string) int64 {
|
||||
val, _ := strconv.ParseInt(i, 10, 64)
|
||||
return val
|
||||
},
|
||||
"inc": func(i int64) int64 {
|
||||
return i + 1
|
||||
},
|
||||
"splitGit": func(i string) []string {
|
||||
return strings.FieldsFunc(i, func(r rune) bool {
|
||||
return r == ',' || r == ' '
|
||||
})
|
||||
},
|
||||
"lines": func(i string) []string {
|
||||
return strings.Split(i, "\n")
|
||||
},
|
||||
"isMarkdown": func(i string) bool {
|
||||
return strings.ToLower(filepath.Ext(i)) == ".md"
|
||||
},
|
||||
"isCsv": func(i string) bool {
|
||||
return strings.ToLower(filepath.Ext(i)) == ".csv"
|
||||
},
|
||||
"csvFile": func(file *git.File) *git.CsvFile {
|
||||
if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
|
||||
return nil
|
||||
}
|
||||
|
||||
csvFile, err := git.ParseCsv(file)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return csvFile
|
||||
},
|
||||
"httpStatusText": http.StatusText,
|
||||
"loadedTime": func(startTime time.Time) string {
|
||||
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
|
||||
},
|
||||
"slug": func(s string) string {
|
||||
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
|
||||
},
|
||||
"avatarUrl": func(user *models.User, noGravatar bool) string {
|
||||
if user.AvatarURL != "" {
|
||||
return user.AvatarURL
|
||||
}
|
||||
|
||||
if user.MD5Hash != "" && !noGravatar {
|
||||
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
|
||||
}
|
||||
|
||||
return defaultAvatar()
|
||||
},
|
||||
"asset": func(jsfile string) string {
|
||||
if dev {
|
||||
return "http://localhost:16157/" + jsfile
|
||||
}
|
||||
return config.C.ExternalUrl + "/" + manifestEntries[jsfile].File
|
||||
},
|
||||
"defaultAvatar": defaultAvatar,
|
||||
}
|
||||
|
||||
var EmbedFS fs.FS
|
||||
|
||||
type Template struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
func Start() {
|
||||
store = sessions.NewCookieStore([]byte("opengist"))
|
||||
gothic.Store = store
|
||||
|
||||
assetsFS := echo.MustSubFS(EmbedFS, "public/assets")
|
||||
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
|
||||
e.Use(dataInit)
|
||||
e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
|
||||
Getter: middleware.MethodFromForm("_method"),
|
||||
}))
|
||||
e.Pre(middleware.RemoveTrailingSlash())
|
||||
e.Pre(middleware.CORS())
|
||||
e.Pre(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
||||
LogURI: true, LogStatus: true, LogMethod: true,
|
||||
LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error {
|
||||
log.Info().Str("URI", v.URI).Int("status", v.Status).Str("method", v.Method).
|
||||
Str("ip", ctx.RealIP()).
|
||||
Msg("HTTP")
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
e.Use(middleware.Recover())
|
||||
e.Use(middleware.Secure())
|
||||
|
||||
e.Renderer = &Template{
|
||||
templates: template.Must(template.New("t").Funcs(fm).ParseFS(EmbedFS, "templates/*/*.html")),
|
||||
}
|
||||
e.HTTPErrorHandler = func(er error, ctx echo.Context) {
|
||||
if err, ok := er.(*echo.HTTPError); ok {
|
||||
if err.Code >= 500 {
|
||||
log.Error().Int("code", err.Code).Err(err.Internal).Msg("HTTP: " + err.Message.(string))
|
||||
}
|
||||
|
||||
setData(ctx, "error", err)
|
||||
if errHtml := htmlWithCode(ctx, err.Code, "error.html"); errHtml != nil {
|
||||
log.Fatal().Err(errHtml).Send()
|
||||
}
|
||||
} else {
|
||||
log.Fatal().Err(er).Send()
|
||||
}
|
||||
}
|
||||
|
||||
e.Use(sessionInit)
|
||||
|
||||
e.Validator = NewValidator()
|
||||
|
||||
if !dev {
|
||||
parseManifestEntries()
|
||||
e.GET("/assets/*", cacheControl(echo.WrapHandler(http.StripPrefix("/assets", http.FileServer(http.FS(assetsFS))))))
|
||||
}
|
||||
|
||||
// Web based routes
|
||||
g1 := e.Group("")
|
||||
{
|
||||
g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "form:_csrf",
|
||||
CookiePath: "/",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
}))
|
||||
g1.Use(csrfInit)
|
||||
|
||||
g1.GET("/", create, logged)
|
||||
g1.POST("/", processCreate, logged)
|
||||
|
||||
g1.GET("/register", register)
|
||||
g1.POST("/register", processRegister)
|
||||
g1.GET("/login", login)
|
||||
g1.POST("/login", processLogin)
|
||||
g1.GET("/logout", logout)
|
||||
g1.GET("/oauth/:provider", oauth)
|
||||
g1.GET("/oauth/:provider/callback", oauthCallback)
|
||||
|
||||
g1.GET("/settings", userSettings, logged)
|
||||
g1.POST("/settings/email", emailProcess, logged)
|
||||
g1.DELETE("/settings/account", accountDeleteProcess, logged)
|
||||
g1.POST("/settings/ssh-keys", sshKeysProcess, logged)
|
||||
g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged)
|
||||
|
||||
g2 := g1.Group("/admin-panel")
|
||||
{
|
||||
g2.Use(adminPermission)
|
||||
g2.GET("", adminIndex)
|
||||
g2.GET("/users", adminUsers)
|
||||
g2.POST("/users/:user/delete", adminUserDelete)
|
||||
g2.GET("/gists", adminGists)
|
||||
g2.POST("/gists/:gist/delete", adminGistDelete)
|
||||
g2.POST("/sync-fs", adminSyncReposFromFS)
|
||||
g2.POST("/sync-db", adminSyncReposFromDB)
|
||||
g2.GET("/configuration", adminConfig)
|
||||
g2.PUT("/set-config", adminSetConfig)
|
||||
}
|
||||
|
||||
g1.GET("/all", allGists, checkRequireLogin)
|
||||
g1.GET("/search", allGists, checkRequireLogin)
|
||||
g1.GET("/:user", allGists, checkRequireLogin)
|
||||
g1.GET("/:user/liked", allGists, checkRequireLogin)
|
||||
g1.GET("/:user/forked", allGists, checkRequireLogin)
|
||||
|
||||
g3 := g1.Group("/:user/:gistname")
|
||||
{
|
||||
g3.Use(checkRequireLogin, gistInit)
|
||||
g3.GET("", gistIndex)
|
||||
g3.GET("/rev/:revision", gistIndex)
|
||||
g3.GET("/revisions", revisions)
|
||||
g3.GET("/archive/:revision", downloadZip)
|
||||
g3.POST("/visibility", toggleVisibility, logged, writePermission)
|
||||
g3.POST("/delete", deleteGist, logged, writePermission)
|
||||
g3.GET("/raw/:revision/:file", rawFile)
|
||||
g3.GET("/edit", edit, logged, writePermission)
|
||||
g3.POST("/edit", processCreate, logged, writePermission)
|
||||
g3.POST("/like", like, logged)
|
||||
g3.GET("/likes", likes)
|
||||
g3.POST("/fork", fork, logged)
|
||||
g3.GET("/forks", forks)
|
||||
}
|
||||
}
|
||||
|
||||
debugStr := ""
|
||||
// Git HTTP routes
|
||||
if config.C.HttpGit {
|
||||
e.Any("/:user/:gistname/*", gitHttp, gistInit)
|
||||
debugStr = " (with Git over HTTP)"
|
||||
}
|
||||
|
||||
e.Any("/*", noRouteFound)
|
||||
|
||||
addr := config.C.HttpHost + ":" + config.C.HttpPort
|
||||
|
||||
if config.C.HttpTLSEnabled {
|
||||
log.Info().Msg("Starting HTTPS server on https://" + addr + debugStr)
|
||||
if err := e.StartTLS(addr, config.C.HttpCertFile, config.C.HttpKeyFile); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start HTTPS server")
|
||||
}
|
||||
} else {
|
||||
log.Info().Msg("Starting HTTP server on http://" + addr + debugStr)
|
||||
if err := e.Start(addr); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start HTTP server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
ctxValue := context.WithValue(ctx.Request().Context(), dataKey, echo.Map{})
|
||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||
setData(ctx, "loadStartTime", time.Now())
|
||||
|
||||
if err := loadSettings(ctx); err != nil {
|
||||
return errorRes(500, "Cannot read settings from database", err)
|
||||
}
|
||||
|
||||
setData(ctx, "c", config.C)
|
||||
|
||||
setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "")
|
||||
setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func sessionInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
sess := getSession(ctx)
|
||||
if sess.Values["user"] != nil {
|
||||
var err error
|
||||
var user *models.User
|
||||
|
||||
if user, err = models.GetUserById(sess.Values["user"].(uint)); err != nil {
|
||||
sess.Values["user"] = nil
|
||||
saveSession(sess, ctx)
|
||||
setData(ctx, "userLogged", nil)
|
||||
return redirect(ctx, "/all")
|
||||
}
|
||||
if user != nil {
|
||||
setData(ctx, "userLogged", user)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
setData(ctx, "userLogged", nil)
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func csrfInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
setCsrfHtmlForm(ctx)
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist")
|
||||
user := getUserLogged(ctx)
|
||||
if !gist.(*models.Gist).CanWrite(user) {
|
||||
return redirect(ctx, "/"+gist.(*models.Gist).User.Username+"/"+gist.(*models.Gist).Uuid)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func adminPermission(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
if user == nil || !user.IsAdmin {
|
||||
return notFound("User not found")
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func logged(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
if user != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
return redirect(ctx, "/all")
|
||||
}
|
||||
}
|
||||
|
||||
func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
if user := getUserLogged(ctx); user != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
require := getData(ctx, "RequireLogin")
|
||||
if require == true {
|
||||
addFlash(ctx, "You must be logged in to access gists", "error")
|
||||
return redirect(ctx, "/login")
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func cacheControl(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Response().Header().Set(echo.HeaderCacheControl, "public, max-age=31536000")
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func noRouteFound(echo.Context) error {
|
||||
return notFound("Page not found")
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
type Asset struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
var manifestEntries map[string]Asset
|
||||
|
||||
func parseManifestEntries() {
|
||||
file, err := EmbedFS.Open("public/manifest.json")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open manifest.json")
|
||||
}
|
||||
byteValue, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to read manifest.json")
|
||||
}
|
||||
if err = json.Unmarshal(byteValue, &manifestEntries); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json")
|
||||
}
|
||||
}
|
||||
|
||||
func defaultAvatar() string {
|
||||
if dev {
|
||||
return "http://localhost:16157/default.png"
|
||||
}
|
||||
return config.C.ExternalUrl + "/" + manifestEntries["default.png"].File
|
||||
}
|
||||
591
internal/web/server.go
Normal file
591
internal/web/server.go
Normal file
@@ -0,0 +1,591 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
htmlpkg "html"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/index"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"github.com/thomiceli/opengist/templates"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/auth"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"github.com/thomiceli/opengist/public"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var (
|
||||
dev bool
|
||||
flashStore *sessions.CookieStore // session store for flash messages
|
||||
userStore *sessions.FilesystemStore // session store for user sessions
|
||||
re = regexp.MustCompile("[^a-z0-9]+")
|
||||
fm = template.FuncMap{
|
||||
"split": strings.Split,
|
||||
"indexByte": strings.IndexByte,
|
||||
"toInt": func(i string) int {
|
||||
val, _ := strconv.Atoi(i)
|
||||
return val
|
||||
},
|
||||
"inc": func(i int) int {
|
||||
return i + 1
|
||||
},
|
||||
"splitGit": func(i string) []string {
|
||||
return strings.FieldsFunc(i, func(r rune) bool {
|
||||
return r == ',' || r == ' '
|
||||
})
|
||||
},
|
||||
"lines": func(i string) []string {
|
||||
return strings.Split(i, "\n")
|
||||
},
|
||||
"isMarkdown": func(i string) bool {
|
||||
return strings.ToLower(filepath.Ext(i)) == ".md"
|
||||
},
|
||||
"isCsv": func(i string) bool {
|
||||
return strings.ToLower(filepath.Ext(i)) == ".csv"
|
||||
},
|
||||
"csvFile": func(file *git.File) *git.CsvFile {
|
||||
if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
|
||||
return nil
|
||||
}
|
||||
|
||||
csvFile, err := git.ParseCsv(file)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return csvFile
|
||||
},
|
||||
"httpStatusText": http.StatusText,
|
||||
"loadedTime": func(startTime time.Time) string {
|
||||
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
|
||||
},
|
||||
"slug": func(s string) string {
|
||||
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
|
||||
},
|
||||
"avatarUrl": func(user *db.User, noGravatar bool) string {
|
||||
if user.AvatarURL != "" {
|
||||
return user.AvatarURL
|
||||
}
|
||||
|
||||
if user.MD5Hash != "" && !noGravatar {
|
||||
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
|
||||
}
|
||||
|
||||
return defaultAvatar()
|
||||
},
|
||||
"asset": asset,
|
||||
"custom": customAsset,
|
||||
"dev": func() bool {
|
||||
return dev
|
||||
},
|
||||
"defaultAvatar": defaultAvatar,
|
||||
"visibilityStr": func(visibility db.Visibility, lowercase bool) string {
|
||||
s := "Public"
|
||||
switch visibility {
|
||||
case 1:
|
||||
s = "Unlisted"
|
||||
case 2:
|
||||
s = "Private"
|
||||
}
|
||||
|
||||
if lowercase {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"unescape": htmlpkg.UnescapeString,
|
||||
"join": func(s ...string) string {
|
||||
return strings.Join(s, "")
|
||||
},
|
||||
"toStr": func(i interface{}) string {
|
||||
return fmt.Sprint(i)
|
||||
},
|
||||
"safe": func(s string) template.HTML {
|
||||
return template.HTML(s)
|
||||
},
|
||||
"dict": func(values ...interface{}) (map[string]interface{}, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, errors.New("invalid dict call")
|
||||
}
|
||||
dict := make(map[string]interface{})
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("dict keys must be strings")
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict, nil
|
||||
},
|
||||
"addMetadataToSearchQuery": addMetadataToSearchQuery,
|
||||
"indexEnabled": index.Enabled,
|
||||
"isUrl": func(s string) bool {
|
||||
_, err := url.ParseRequestURI(s)
|
||||
return err == nil
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
echo *echo.Echo
|
||||
dev bool
|
||||
}
|
||||
|
||||
func NewServer(isDev bool, sessionsPath string) *Server {
|
||||
dev = isDev
|
||||
flashStore = sessions.NewCookieStore([]byte("opengist"))
|
||||
userStore = sessions.NewFilesystemStore(sessionsPath,
|
||||
utils.ReadKey(path.Join(sessionsPath, "session-auth.key")),
|
||||
utils.ReadKey(path.Join(sessionsPath, "session-encrypt.key")),
|
||||
)
|
||||
userStore.MaxLength(10 * 1024)
|
||||
gothic.Store = userStore
|
||||
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
|
||||
if err := i18n.Locales.LoadAll(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to load locales")
|
||||
}
|
||||
|
||||
e.Use(dataInit)
|
||||
e.Use(locale)
|
||||
e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
|
||||
Getter: middleware.MethodFromForm("_method"),
|
||||
}))
|
||||
e.Pre(middleware.RemoveTrailingSlash())
|
||||
e.Pre(middleware.CORS())
|
||||
e.Pre(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
||||
LogURI: true, LogStatus: true, LogMethod: true,
|
||||
LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error {
|
||||
log.Info().Str("URI", v.URI).Int("status", v.Status).Str("method", v.Method).
|
||||
Str("ip", ctx.RealIP()).
|
||||
Msg("HTTP")
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
e.Use(middleware.Recover())
|
||||
e.Use(middleware.Secure())
|
||||
|
||||
t := template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html"))
|
||||
customPattern := filepath.Join(config.GetHomeDir(), "custom", "*.html")
|
||||
matches, err := filepath.Glob(customPattern)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to check for custom templates")
|
||||
}
|
||||
if len(matches) > 0 {
|
||||
t, err = t.ParseGlob(customPattern)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to parse custom templates")
|
||||
}
|
||||
}
|
||||
e.Renderer = &Template{
|
||||
templates: t,
|
||||
}
|
||||
|
||||
e.HTTPErrorHandler = func(er error, ctx echo.Context) {
|
||||
if err, ok := er.(*echo.HTTPError); ok {
|
||||
if err.Code >= 500 {
|
||||
log.Error().Int("code", err.Code).Err(err.Internal).Msg("HTTP: " + err.Message.(string))
|
||||
}
|
||||
|
||||
setData(ctx, "error", err)
|
||||
if errHtml := htmlWithCode(ctx, err.Code, "error.html"); errHtml != nil {
|
||||
log.Fatal().Err(errHtml).Send()
|
||||
}
|
||||
} else {
|
||||
log.Fatal().Err(er).Send()
|
||||
}
|
||||
}
|
||||
|
||||
e.Use(sessionInit)
|
||||
|
||||
e.Validator = utils.NewValidator()
|
||||
|
||||
if !dev {
|
||||
parseManifestEntries()
|
||||
}
|
||||
|
||||
// Web based routes
|
||||
g1 := e.Group("")
|
||||
{
|
||||
if !dev {
|
||||
g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "form:_csrf",
|
||||
CookiePath: "/",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
}))
|
||||
g1.Use(csrfInit)
|
||||
}
|
||||
|
||||
g1.GET("/", create, logged)
|
||||
g1.POST("/", processCreate, logged)
|
||||
g1.GET("/preview", preview, logged)
|
||||
|
||||
g1.GET("/healthcheck", healthcheck)
|
||||
|
||||
g1.GET("/register", register)
|
||||
g1.POST("/register", processRegister)
|
||||
g1.GET("/login", login)
|
||||
g1.POST("/login", processLogin)
|
||||
g1.GET("/logout", logout)
|
||||
g1.GET("/oauth/:provider", oauth)
|
||||
g1.GET("/oauth/:provider/callback", oauthCallback)
|
||||
|
||||
g1.GET("/settings", userSettings, logged)
|
||||
g1.POST("/settings/email", emailProcess, logged)
|
||||
g1.DELETE("/settings/account", accountDeleteProcess, logged)
|
||||
g1.POST("/settings/ssh-keys", sshKeysProcess, logged)
|
||||
g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged)
|
||||
g1.PUT("/settings/password", passwordProcess, logged)
|
||||
g1.PUT("/settings/username", usernameProcess, logged)
|
||||
g2 := g1.Group("/admin-panel")
|
||||
{
|
||||
g2.Use(adminPermission)
|
||||
g2.GET("", adminIndex)
|
||||
g2.GET("/users", adminUsers)
|
||||
g2.POST("/users/:user/delete", adminUserDelete)
|
||||
g2.GET("/gists", adminGists)
|
||||
g2.POST("/gists/:gist/delete", adminGistDelete)
|
||||
g2.GET("/invitations", adminInvitations)
|
||||
g2.POST("/invitations", adminInvitationsCreate)
|
||||
g2.POST("/invitations/:id/delete", adminInvitationsDelete)
|
||||
g2.POST("/sync-fs", adminSyncReposFromFS)
|
||||
g2.POST("/sync-db", adminSyncReposFromDB)
|
||||
g2.POST("/gc-repos", adminGcRepos)
|
||||
g2.POST("/sync-previews", adminSyncGistPreviews)
|
||||
g2.POST("/reset-hooks", adminResetHooks)
|
||||
g2.POST("/index-gists", adminIndexGists)
|
||||
g2.GET("/configuration", adminConfig)
|
||||
g2.PUT("/set-config", adminSetConfig)
|
||||
}
|
||||
|
||||
if config.C.HttpGit {
|
||||
e.Any("/init/*", gitHttp, gistNewPushSoftInit)
|
||||
}
|
||||
|
||||
g1.GET("/all", allGists, checkRequireLogin)
|
||||
|
||||
if index.Enabled() {
|
||||
g1.GET("/search", search, checkRequireLogin)
|
||||
} else {
|
||||
g1.GET("/search", allGists, checkRequireLogin)
|
||||
}
|
||||
|
||||
g1.GET("/:user", allGists, checkRequireLogin)
|
||||
g1.GET("/:user/liked", allGists, checkRequireLogin)
|
||||
g1.GET("/:user/forked", allGists, checkRequireLogin)
|
||||
|
||||
g3 := g1.Group("/:user/:gistname")
|
||||
{
|
||||
g3.Use(makeCheckRequireLogin(true), gistInit)
|
||||
g3.GET("", gistIndex)
|
||||
g3.GET("/rev/:revision", gistIndex)
|
||||
g3.GET("/revisions", revisions)
|
||||
g3.GET("/archive/:revision", downloadZip)
|
||||
g3.POST("/visibility", editVisibility, logged, writePermission)
|
||||
g3.POST("/delete", deleteGist, logged, writePermission)
|
||||
g3.GET("/raw/:revision/:file", rawFile)
|
||||
g3.GET("/download/:revision/:file", downloadFile)
|
||||
g3.GET("/edit", edit, logged, writePermission)
|
||||
g3.POST("/edit", processCreate, logged, writePermission)
|
||||
g3.POST("/like", like, logged)
|
||||
g3.GET("/likes", likes, checkRequireLogin)
|
||||
g3.POST("/fork", fork, logged)
|
||||
g3.GET("/forks", forks, checkRequireLogin)
|
||||
g3.PUT("/checkbox", checkbox, logged, writePermission)
|
||||
}
|
||||
}
|
||||
|
||||
customFs := os.DirFS(filepath.Join(config.GetHomeDir(), "custom"))
|
||||
e.GET("/assets/*", func(ctx echo.Context) error {
|
||||
if _, err := public.Files.Open(path.Join("assets", ctx.Param("*"))); !dev && err == nil {
|
||||
return echo.WrapHandler(http.FileServer(http.FS(public.Files)))(ctx)
|
||||
}
|
||||
|
||||
// if the custom file is an .html template, render it
|
||||
if strings.HasSuffix(ctx.Param("*"), ".html") {
|
||||
if err := html(ctx, ctx.Param("*")); err != nil {
|
||||
return notFound("Page not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return echo.WrapHandler(http.StripPrefix("/assets/", http.FileServer(http.FS(customFs))))(ctx)
|
||||
})
|
||||
|
||||
// Git HTTP routes
|
||||
if config.C.HttpGit {
|
||||
e.Any("/:user/:gistname/*", gitHttp, gistSoftInit)
|
||||
}
|
||||
|
||||
e.Any("/*", noRouteFound)
|
||||
|
||||
return &Server{echo: e, dev: dev}
|
||||
}
|
||||
|
||||
func (s *Server) Start() {
|
||||
addr := config.C.HttpHost + ":" + config.C.HttpPort
|
||||
|
||||
log.Info().Msg("Starting HTTP server on http://" + addr)
|
||||
if err := s.echo.Start(addr); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal().Err(err).Msg("Failed to start HTTP server")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
if err := s.echo.Close(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to stop HTTP server")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.echo.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
ctxValue := context.WithValue(ctx.Request().Context(), dataKey, echo.Map{})
|
||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||
setData(ctx, "loadStartTime", time.Now())
|
||||
|
||||
if err := loadSettings(ctx); err != nil {
|
||||
return errorRes(500, "Cannot read settings from database", err)
|
||||
}
|
||||
|
||||
setData(ctx, "c", config.C)
|
||||
|
||||
setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "")
|
||||
setData(ctx, "gitlabOauth", config.C.GitlabClientKey != "" && config.C.GitlabSecret != "")
|
||||
setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
|
||||
setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
|
||||
|
||||
httpProtocol := "http"
|
||||
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
|
||||
httpProtocol = "https"
|
||||
}
|
||||
setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))
|
||||
|
||||
var baseHttpUrl string
|
||||
// if a custom external url is set, use it
|
||||
if config.C.ExternalUrl != "" {
|
||||
baseHttpUrl = config.C.ExternalUrl
|
||||
} else {
|
||||
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
|
||||
}
|
||||
|
||||
setData(ctx, "baseHttpUrl", baseHttpUrl)
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func locale(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
// Check URL arguments
|
||||
lang := ctx.Request().URL.Query().Get("lang")
|
||||
changeLang := lang != ""
|
||||
|
||||
// Then check cookies
|
||||
if len(lang) == 0 {
|
||||
cookie, _ := ctx.Request().Cookie("lang")
|
||||
if cookie != nil {
|
||||
lang = cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Check again in case someone changes the supported language list.
|
||||
if lang != "" && !i18n.Locales.HasLocale(lang) {
|
||||
lang = ""
|
||||
changeLang = false
|
||||
}
|
||||
|
||||
// 3.Then check from 'Accept-Language' header.
|
||||
if len(lang) == 0 {
|
||||
tags, _, _ := language.ParseAcceptLanguage(ctx.Request().Header.Get("Accept-Language"))
|
||||
lang = i18n.Locales.MatchTag(tags)
|
||||
}
|
||||
|
||||
if changeLang {
|
||||
ctx.SetCookie(&http.Cookie{Name: "lang", Value: lang, Path: "/", MaxAge: 1<<31 - 1})
|
||||
}
|
||||
|
||||
localeUsed, err := i18n.Locales.GetLocale(lang)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot get locale", err)
|
||||
}
|
||||
|
||||
setData(ctx, "localeName", localeUsed.Name)
|
||||
setData(ctx, "locale", localeUsed)
|
||||
setData(ctx, "allLocales", i18n.Locales.Locales)
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func sessionInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
sess := getSession(ctx)
|
||||
if sess.Values["user"] != nil {
|
||||
var err error
|
||||
var user *db.User
|
||||
|
||||
if user, err = db.GetUserById(sess.Values["user"].(uint)); err != nil {
|
||||
sess.Values["user"] = nil
|
||||
saveSession(sess, ctx)
|
||||
setData(ctx, "userLogged", nil)
|
||||
return redirect(ctx, "/all")
|
||||
}
|
||||
if user != nil {
|
||||
setData(ctx, "userLogged", user)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
setData(ctx, "userLogged", nil)
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func csrfInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
setCsrfHtmlForm(ctx)
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
gist := getData(ctx, "gist")
|
||||
user := getUserLogged(ctx)
|
||||
if !gist.(*db.Gist).CanWrite(user) {
|
||||
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier())
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func adminPermission(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
if user == nil || !user.IsAdmin {
|
||||
return notFound("User not found")
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func logged(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
if user != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
return redirect(ctx, "/all")
|
||||
}
|
||||
}
|
||||
|
||||
func makeCheckRequireLogin(isSingleGistAccess bool) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
if user := getUserLogged(ctx); user != nil {
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(ContextAuthInfo{ctx}, isSingleGistAccess)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to check if unauthenticated access is allowed")
|
||||
}
|
||||
|
||||
if !allow {
|
||||
addFlash(ctx, tr(ctx, "flash.auth.must-be-logged-in"), "error")
|
||||
return redirect(ctx, "/login")
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return makeCheckRequireLogin(false)(next)
|
||||
}
|
||||
|
||||
func noRouteFound(echo.Context) error {
|
||||
return notFound("Page not found")
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
type Asset struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
var manifestEntries map[string]Asset
|
||||
|
||||
func parseManifestEntries() {
|
||||
file, err := public.Files.Open("manifest.json")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open manifest.json")
|
||||
}
|
||||
byteValue, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to read manifest.json")
|
||||
}
|
||||
if err = json.Unmarshal(byteValue, &manifestEntries); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json")
|
||||
}
|
||||
}
|
||||
|
||||
func defaultAvatar() string {
|
||||
if dev {
|
||||
return "http://localhost:16157/default.png"
|
||||
}
|
||||
return config.C.ExternalUrl + "/" + manifestEntries["default.png"].File
|
||||
}
|
||||
|
||||
func asset(file string) string {
|
||||
if dev {
|
||||
return "http://localhost:16157/" + file
|
||||
}
|
||||
return config.C.ExternalUrl + "/" + manifestEntries[file].File
|
||||
}
|
||||
|
||||
func customAsset(file string) string {
|
||||
assetpath, err := url.JoinPath("/", "assets", file)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to join path for custom file %s", file)
|
||||
}
|
||||
return config.C.ExternalUrl + assetpath
|
||||
}
|
||||
@@ -3,25 +3,33 @@ package web
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"github.com/thomiceli/opengist/internal/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func userSettings(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
|
||||
keys, err := models.GetSSHKeysByUserID(user.ID)
|
||||
keys, err := db.GetSSHKeysByUserID(user.ID)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot get SSH keys", err)
|
||||
}
|
||||
|
||||
setData(ctx, "email", user.Email)
|
||||
setData(ctx, "sshKeys", keys)
|
||||
setData(ctx, "htmlTitle", "Settings")
|
||||
setData(ctx, "hasPassword", user.Password != "")
|
||||
setData(ctx, "htmlTitle", trH(ctx, "settings"))
|
||||
return html(ctx, "settings.html")
|
||||
}
|
||||
|
||||
@@ -44,7 +52,7 @@ func emailProcess(ctx echo.Context) error {
|
||||
return errorRes(500, "Cannot update email", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "Email updated", "success")
|
||||
addFlash(ctx, tr(ctx, "flash.user.email-updated"), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
@@ -61,13 +69,13 @@ func accountDeleteProcess(ctx echo.Context) error {
|
||||
func sshKeysProcess(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
|
||||
var dto = new(models.SSHKeyDTO)
|
||||
dto := new(db.SSHKeyDTO)
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, "Cannot bind data", err)
|
||||
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
|
||||
}
|
||||
|
||||
if err := ctx.Validate(dto); err != nil {
|
||||
addFlash(ctx, validationMessages(&err), "error")
|
||||
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
key := dto.ToSSHKey()
|
||||
@@ -76,28 +84,35 @@ func sshKeysProcess(ctx echo.Context) error {
|
||||
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||
if err != nil {
|
||||
addFlash(ctx, "Invalid SSH key", "error")
|
||||
addFlash(ctx, tr(ctx, "flash.user.invalid-ssh-key"), "error")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
||||
|
||||
if exists, err := db.SSHKeyDoesExists(key.Content); exists {
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot check if SSH key exists", err)
|
||||
}
|
||||
addFlash(ctx, tr(ctx, "settings.ssh-key-exists"), "error")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
if err := key.Create(); err != nil {
|
||||
return errorRes(500, "Cannot add SSH key", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "SSH key added", "success")
|
||||
addFlash(ctx, tr(ctx, "flash.user.ssh-key-added"), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
func sshKeysDelete(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
keyId, err := strconv.Atoi(ctx.Param("id"))
|
||||
|
||||
if err != nil {
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
key, err := models.GetSSHKeyByID(uint(keyId))
|
||||
key, err := db.GetSSHKeyByID(uint(keyId))
|
||||
|
||||
if err != nil || key.UserID != user.ID {
|
||||
return redirect(ctx, "/settings")
|
||||
@@ -107,6 +122,73 @@ func sshKeysDelete(ctx echo.Context) error {
|
||||
return errorRes(500, "Cannot delete SSH key", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "SSH key deleted", "success")
|
||||
addFlash(ctx, tr(ctx, "flash.user.ssh-key-deleted"), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
func passwordProcess(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
|
||||
dto := new(db.UserDTO)
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
|
||||
}
|
||||
dto.Username = user.Username
|
||||
|
||||
if err := ctx.Validate(dto); err != nil {
|
||||
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
|
||||
return html(ctx, "settings.html")
|
||||
}
|
||||
|
||||
password, err := utils.Argon2id.Hash(dto.Password)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot hash password", err)
|
||||
}
|
||||
user.Password = password
|
||||
|
||||
if err = user.Update(); err != nil {
|
||||
return errorRes(500, "Cannot update password", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, tr(ctx, "flash.user.password-updated"), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
func usernameProcess(ctx echo.Context) error {
|
||||
user := getUserLogged(ctx)
|
||||
|
||||
dto := new(db.UserDTO)
|
||||
if err := ctx.Bind(dto); err != nil {
|
||||
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
|
||||
}
|
||||
dto.Password = user.Password
|
||||
|
||||
if err := ctx.Validate(dto); err != nil {
|
||||
addFlash(ctx, utils.ValidationMessages(&err, getData(ctx, "locale").(*i18n.Locale)), "error")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
if exists, err := db.UserExists(dto.Username); err != nil || exists {
|
||||
addFlash(ctx, tr(ctx, "flash.auth.username-exists"), "error")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
sourceDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(user.Username))
|
||||
destinationDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(dto.Username))
|
||||
|
||||
if _, err := os.Stat(sourceDir); !os.IsNotExist(err) {
|
||||
err := os.Rename(sourceDir, destinationDir)
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot rename user directory", err)
|
||||
}
|
||||
}
|
||||
|
||||
user.Username = dto.Username
|
||||
|
||||
if err := user.Update(); err != nil {
|
||||
return errorRes(500, "Cannot update username", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, tr(ctx, "flash.user.username-updated"), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
318
internal/web/test/auth_test.go
Normal file
318
internal/web/test/auth_test.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
err = s.request("GET", "/", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.request("GET", "/register", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
user1db, err := db.GetUserById(1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, user1.Username, user1db.Username)
|
||||
require.True(t, user1db.IsAdmin)
|
||||
|
||||
err = s.request("GET", "/", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
user2 := db.UserDTO{Username: "thomas", Password: "azeaze"}
|
||||
err = s.request("POST", "/register", user2, 200)
|
||||
require.Error(t, err)
|
||||
|
||||
user3 := db.UserDTO{Username: "kaguya", Password: "kaguya"}
|
||||
register(t, s, user3)
|
||||
|
||||
user3db, err := db.GetUserById(2)
|
||||
require.NoError(t, err)
|
||||
require.False(t, user3db.IsAdmin)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
count, err := db.CountAll(db.User{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(2), count)
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
err = s.request("GET", "/login", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
login(t, s, user1)
|
||||
require.NotEmpty(t, s.sessionCookie)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
user2 := db.UserDTO{Username: "thomas", Password: "azeaze"}
|
||||
user3 := db.UserDTO{Username: "azeaze", Password: ""}
|
||||
|
||||
err = s.request("POST", "/login", user2, 302)
|
||||
require.Empty(t, s.sessionCookie)
|
||||
require.Error(t, err)
|
||||
|
||||
err = s.request("POST", "/login", user3, 302)
|
||||
require.Empty(t, s.sessionCookie)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func register(t *testing.T, s *testServer, user db.UserDTO) {
|
||||
err := s.request("POST", "/register", user, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func login(t *testing.T, s *testServer, user db.UserDTO) {
|
||||
err := s.request("POST", "/login", user, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
type settingSet struct {
|
||||
key string `form:"key"`
|
||||
value string `form:"value"`
|
||||
}
|
||||
|
||||
func TestAnonymous(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
user := db.UserDTO{Username: "thomas", Password: "azeaze"}
|
||||
register(t, s, user)
|
||||
|
||||
err = s.request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
}
|
||||
err = s.request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.request("GET", "/all", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
cookie := s.sessionCookie
|
||||
s.sessionCookie = ""
|
||||
|
||||
err = s.request("GET", "/all", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should redirect to login if RequireLogin
|
||||
err = s.request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = cookie
|
||||
|
||||
err = s.request("PUT", "/admin-panel/set-config", settingSet{"allow-gists-without-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
// Should return results
|
||||
err = s.request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestGitOperations(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
admin := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, admin)
|
||||
s.sessionCookie = ""
|
||||
register(t, s, db.UserDTO{Username: "fujiwara", Password: "fujiwara"})
|
||||
s.sessionCookie = ""
|
||||
register(t, s, db.UserDTO{Username: "kaguya", Password: "kaguya"})
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "kaguya-pub-gist",
|
||||
URL: "kaguya-pub-gist",
|
||||
Description: "kaguya's first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PublicVisibility,
|
||||
},
|
||||
Name: []string{"kaguya-file.txt"},
|
||||
Content: []string{
|
||||
"yeah",
|
||||
},
|
||||
}
|
||||
err = s.request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "kaguya-unl-gist",
|
||||
URL: "kaguya-unl-gist",
|
||||
Description: "kaguya's second gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.UnlistedVisibility,
|
||||
},
|
||||
Name: []string{"kaguya-file.txt"},
|
||||
Content: []string{
|
||||
"cool",
|
||||
},
|
||||
}
|
||||
err = s.request("POST", "/", gist2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3 := db.GistDTO{
|
||||
Title: "kaguya-priv-gist",
|
||||
URL: "kaguya-priv-gist",
|
||||
Description: "kaguya's second gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.PrivateVisibility,
|
||||
},
|
||||
Name: []string{"kaguya-file.txt"},
|
||||
Content: []string{
|
||||
"super",
|
||||
},
|
||||
}
|
||||
err = s.request("POST", "/", gist3, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gitOperations := func(credentials, owner, url, filename string, expectErrorClone, expectErrorCheck, expectErrorPush bool) {
|
||||
fmt.Println("Testing", credentials, url, expectErrorClone, expectErrorCheck, expectErrorPush)
|
||||
err := clientGitClone(credentials, owner, url)
|
||||
if expectErrorClone {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = clientCheckRepo(url, filename)
|
||||
if expectErrorCheck {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = clientGitPush(url)
|
||||
if expectErrorPush {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
credentials string
|
||||
user string
|
||||
url string
|
||||
expectErrorClone bool
|
||||
expectErrorCheck bool
|
||||
expectErrorPush bool
|
||||
}{
|
||||
{":", "kaguya", "kaguya-pub-gist", false, false, true},
|
||||
{":", "kaguya", "kaguya-unl-gist", false, false, true},
|
||||
{":", "kaguya", "kaguya-priv-gist", true, true, true},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", false, false, false},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", false, false, false},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", false, false, false},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", false, false, true},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", false, false, true},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", true, true, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
|
||||
}
|
||||
|
||||
login(t, s, admin)
|
||||
err = s.request("PUT", "/admin-panel/set-config", settingSet{"require-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
testsRequireLogin := []struct {
|
||||
credentials string
|
||||
user string
|
||||
url string
|
||||
expectErrorClone bool
|
||||
expectErrorCheck bool
|
||||
expectErrorPush bool
|
||||
}{
|
||||
{":", "kaguya", "kaguya-pub-gist", true, true, true},
|
||||
{":", "kaguya", "kaguya-unl-gist", true, true, true},
|
||||
{":", "kaguya", "kaguya-priv-gist", true, true, true},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", false, false, false},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", false, false, false},
|
||||
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", false, false, false},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", false, false, true},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", false, false, true},
|
||||
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", true, true, true},
|
||||
}
|
||||
|
||||
for _, test := range testsRequireLogin {
|
||||
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
|
||||
}
|
||||
|
||||
login(t, s, admin)
|
||||
err = s.request("PUT", "/admin-panel/set-config", settingSet{"allow-gists-without-login", "1"}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range tests {
|
||||
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
|
||||
}
|
||||
}
|
||||
|
||||
func clientGitClone(creds string, user string, url string) error {
|
||||
return exec.Command("git", "clone", "http://"+creds+"@localhost:6157/"+user+"/"+url, path.Join(config.GetHomeDir(), "tmp", url)).Run()
|
||||
}
|
||||
|
||||
func clientGitPush(url string) error {
|
||||
f, err := os.Create(path.Join(config.GetHomeDir(), "tmp", url, "newfile.txt"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
|
||||
_ = exec.Command("git", "-C", path.Join(config.GetHomeDir(), "tmp", url), "add", "newfile.txt").Run()
|
||||
_ = exec.Command("git", "-C", path.Join(config.GetHomeDir(), "tmp", url), "commit", "-m", "new file").Run()
|
||||
err = exec.Command("git", "-C", path.Join(config.GetHomeDir(), "tmp", url), "push", "origin", "master").Run()
|
||||
|
||||
_ = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", url))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func clientCheckRepo(url string, file string) error {
|
||||
_, err := os.ReadFile(path.Join(config.GetHomeDir(), "tmp", url, file))
|
||||
return err
|
||||
}
|
||||
271
internal/web/test/gist_test.go
Normal file
271
internal/web/test/gist_test.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
)
|
||||
|
||||
func TestGists(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
err = s.request("GET", "/", nil, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
err = s.request("GET", "/all", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.request("POST", "/", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
}
|
||||
err = s.request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), gist1db.ID)
|
||||
require.Equal(t, gist1.Title, gist1db.Title)
|
||||
require.Equal(t, gist1.Description, gist1db.Description)
|
||||
require.Regexp(t, "[a-f0-9]{32}", gist1db.Uuid)
|
||||
require.Equal(t, user1.Username, gist1db.User.Username)
|
||||
|
||||
err = s.request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1files, err := git.GetFilesOfRepository(gist1db.User.Username, gist1db.Uuid, "HEAD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(gist1files))
|
||||
|
||||
gist1fileContent, _, err := git.GetFileContent(gist1db.User.Username, gist1db.Uuid, "HEAD", gist1.Name[0], false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1.Content[0], gist1fileContent)
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "gist2",
|
||||
Description: "my second gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
}
|
||||
err = s.request("POST", "/", gist2, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3 := db.GistDTO{
|
||||
Title: "gist3",
|
||||
Description: "my third gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{""},
|
||||
Content: []string{"yeah"},
|
||||
}
|
||||
err = s.request("POST", "/", gist3, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
gist3files, err := git.GetFilesOfRepository(gist3db.User.Username, gist3db.Uuid, "HEAD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "gistfile1.txt", gist3files[0])
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", nil, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1.Name = []string{"gist1.txt"}
|
||||
gist1.Content = []string{"only want one gist"}
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1files, err = git.GetFilesOfRepository(gist1db.User.Username, gist1db.Uuid, "HEAD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(gist1files))
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/delete", nil, 302)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestVisibility(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: db.UnlistedVisibility,
|
||||
},
|
||||
Name: []string{""},
|
||||
Content: []string{"yeah"},
|
||||
}
|
||||
err = s.request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.UnlistedVisibility, gist1db.Private)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.PrivateVisibility}, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.PrivateVisibility, gist1db.Private)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.PublicVisibility}, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.PublicVisibility, gist1db.Private)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.UnlistedVisibility}, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, db.UnlistedVisibility, gist1db.Private)
|
||||
}
|
||||
|
||||
func TestLikeFork(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 1,
|
||||
},
|
||||
Name: []string{""},
|
||||
Content: []string{"yeah"},
|
||||
}
|
||||
err = s.request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.sessionCookie = ""
|
||||
|
||||
user2 := db.UserDTO{Username: "kaguya", Password: "kaguya"}
|
||||
register(t, s, user2)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, gist1db.NbLikes)
|
||||
likeCount, err := db.CountAll(db.Like{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), likeCount)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/like", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, gist1db.NbLikes)
|
||||
likeCount, err = db.CountAll(db.Like{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), likeCount)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/like", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist1db, err = db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, gist1db.NbLikes)
|
||||
likeCount, err = db.CountAll(db.Like{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), likeCount)
|
||||
|
||||
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/fork", nil, 302)
|
||||
require.NoError(t, err)
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1db.Title, gist2db.Title)
|
||||
require.Equal(t, gist1db.Description, gist2db.Description)
|
||||
require.Equal(t, gist1db.Private, gist2db.Private)
|
||||
require.Equal(t, user2.Username, gist2db.User.Username)
|
||||
}
|
||||
|
||||
func TestCustomUrl(t *testing.T) {
|
||||
setup(t)
|
||||
s, err := newTestServer()
|
||||
require.NoError(t, err, "Failed to create test server")
|
||||
defer teardown(t, s)
|
||||
|
||||
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
|
||||
register(t, s, user1)
|
||||
|
||||
gist1 := db.GistDTO{
|
||||
Title: "gist1",
|
||||
URL: "my-gist",
|
||||
Description: "my first gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
}
|
||||
err = s.request("POST", "/", gist1, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist1db, err := db.GetGistByID("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(1), gist1db.ID)
|
||||
require.Equal(t, gist1.Title, gist1db.Title)
|
||||
require.Equal(t, gist1.Description, gist1db.Description)
|
||||
require.Regexp(t, "[a-f0-9]{32}", gist1db.Uuid)
|
||||
require.Equal(t, gist1.URL, gist1db.URL)
|
||||
require.Equal(t, user1.Username, gist1db.User.Username)
|
||||
|
||||
gist1dbUuid, err := db.GetGist(user1.Username, gist1db.Uuid)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1db, gist1dbUuid)
|
||||
|
||||
gist1dbUrl, err := db.GetGist(user1.Username, gist1.URL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gist1db, gist1dbUrl)
|
||||
|
||||
require.Equal(t, gist1.URL, gist1db.Identifier())
|
||||
|
||||
gist2 := db.GistDTO{
|
||||
Title: "gist2",
|
||||
Description: "my second gist",
|
||||
VisibilityDTO: db.VisibilityDTO{
|
||||
Private: 0,
|
||||
},
|
||||
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
|
||||
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
|
||||
}
|
||||
err = s.request("POST", "/", gist2, 302)
|
||||
require.NoError(t, err)
|
||||
|
||||
gist2db, err := db.GetGistByID("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, gist2db.Uuid, gist2db.Identifier())
|
||||
require.NotEqual(t, gist2db.URL, gist2db.Identifier())
|
||||
}
|
||||
188
internal/web/test/server.go
Normal file
188
internal/web/test/server.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/memdb"
|
||||
"github.com/thomiceli/opengist/internal/web"
|
||||
)
|
||||
|
||||
type testServer struct {
|
||||
server *web.Server
|
||||
sessionCookie string
|
||||
}
|
||||
|
||||
func newTestServer() (*testServer, error) {
|
||||
s := &testServer{
|
||||
server: web.NewServer(true, path.Join(config.GetHomeDir(), "tmp", "sessions")),
|
||||
}
|
||||
|
||||
go s.start()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *testServer) start() {
|
||||
s.server.Start()
|
||||
}
|
||||
|
||||
func (s *testServer) stop() {
|
||||
s.server.Stop()
|
||||
}
|
||||
|
||||
func (s *testServer) request(method, uri string, data interface{}, expectedCode int) error {
|
||||
var bodyReader io.Reader
|
||||
if method == http.MethodPost || method == http.MethodPut {
|
||||
values := structToURLValues(data)
|
||||
bodyReader = strings.NewReader(values.Encode())
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(method, "http://localhost:6157"+uri, bodyReader)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if method == http.MethodPost || method == http.MethodPut {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
if s.sessionCookie != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: s.sessionCookie})
|
||||
}
|
||||
|
||||
s.server.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != expectedCode {
|
||||
return fmt.Errorf("unexpected status code %d, expected %d", w.Code, expectedCode)
|
||||
}
|
||||
|
||||
if method == http.MethodPost {
|
||||
if strings.Contains(uri, "/login") || strings.Contains(uri, "/register") {
|
||||
cookie := ""
|
||||
h := w.Header().Get("Set-Cookie")
|
||||
parts := strings.Split(h, "; ")
|
||||
for _, p := range parts {
|
||||
if strings.HasPrefix(p, "session=") {
|
||||
cookie = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if cookie == "" {
|
||||
return errors.New("unable to find access session token in response headers")
|
||||
}
|
||||
s.sessionCookie = strings.TrimPrefix(cookie, "session=")
|
||||
} else if strings.Contains(uri, "/logout") {
|
||||
s.sessionCookie = ""
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func structToURLValues(s interface{}) url.Values {
|
||||
v := url.Values{}
|
||||
if s == nil {
|
||||
return v
|
||||
}
|
||||
|
||||
rValue := reflect.ValueOf(s)
|
||||
if rValue.Kind() != reflect.Struct {
|
||||
return v
|
||||
}
|
||||
|
||||
for i := 0; i < rValue.NumField(); i++ {
|
||||
field := rValue.Type().Field(i)
|
||||
tag := field.Tag.Get("form")
|
||||
if tag != "" || field.Anonymous {
|
||||
if field.Type.Kind() == reflect.Int {
|
||||
fieldValue := rValue.Field(i).Int()
|
||||
v.Add(tag, strconv.FormatInt(fieldValue, 10))
|
||||
} else if field.Type.Kind() == reflect.Slice {
|
||||
fieldValue := rValue.Field(i).Interface().([]string)
|
||||
for _, va := range fieldValue {
|
||||
v.Add(tag, va)
|
||||
}
|
||||
} else if field.Type.Kind() == reflect.Struct {
|
||||
for key, val := range structToURLValues(rValue.Field(i).Interface()) {
|
||||
for _, vv := range val {
|
||||
v.Add(key, vv)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fieldValue := rValue.Field(i).String()
|
||||
v.Add(tag, fieldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func setup(t *testing.T) {
|
||||
_ = os.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1")
|
||||
|
||||
err := config.InitConfig("", io.Discard)
|
||||
require.NoError(t, err, "Could not init config")
|
||||
|
||||
err = os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755)
|
||||
require.NoError(t, err, "Could not create Opengist home directory")
|
||||
|
||||
git.ReposDirectory = path.Join("tests")
|
||||
|
||||
config.C.IndexEnabled = false
|
||||
config.C.LogLevel = "debug"
|
||||
config.InitLog()
|
||||
|
||||
homePath := config.GetHomeDir()
|
||||
log.Info().Msg("Data directory: " + homePath)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(homePath, "tmp", "sessions"), 0755)
|
||||
require.NoError(t, err, "Could not create sessions directory")
|
||||
|
||||
err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
|
||||
require.NoError(t, err, "Could not create tmp repos directory")
|
||||
|
||||
err = db.Setup("file::memory:", true)
|
||||
require.NoError(t, err, "Could not initialize database")
|
||||
|
||||
err = memdb.Setup()
|
||||
require.NoError(t, err, "Could not initialize in memory database")
|
||||
|
||||
// err = index.Open(filepath.Join(homePath, "testsindex", "opengist.index"))
|
||||
// require.NoError(t, err, "Could not open index")
|
||||
}
|
||||
|
||||
func teardown(t *testing.T, s *testServer) {
|
||||
s.stop()
|
||||
|
||||
err := db.Close()
|
||||
require.NoError(t, err, "Could not close database")
|
||||
|
||||
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tests"))
|
||||
require.NoError(t, err, "Could not remove repos directory")
|
||||
|
||||
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "repos"))
|
||||
require.NoError(t, err, "Could not remove repos directory")
|
||||
|
||||
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "sessions"))
|
||||
require.NoError(t, err, "Could not remove repos directory")
|
||||
|
||||
// err = os.RemoveAll(path.Join(config.C.OpengistHome, "testsindex"))
|
||||
// require.NoError(t, err, "Could not remove repos directory")
|
||||
|
||||
// err = index.Close()
|
||||
// require.NoError(t, err, "Could not close index")
|
||||
}
|
||||
@@ -2,17 +2,12 @@ package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -35,6 +30,10 @@ func getData(ctx echo.Context, key string) any {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
func dataMap(ctx echo.Context) echo.Map {
|
||||
return ctx.Request().Context().Value(dataKey).(echo.Map)
|
||||
}
|
||||
|
||||
func html(ctx echo.Context, template string) error {
|
||||
return htmlWithCode(ctx, 200, template)
|
||||
}
|
||||
@@ -60,16 +59,16 @@ func errorRes(code int, message string, err error) error {
|
||||
return &echo.HTTPError{Code: code, Message: message, Internal: err}
|
||||
}
|
||||
|
||||
func getUserLogged(ctx echo.Context) *models.User {
|
||||
func getUserLogged(ctx echo.Context) *db.User {
|
||||
user := getData(ctx, "userLogged")
|
||||
if user != nil {
|
||||
return user.(*models.User)
|
||||
return user.(*db.User)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setErrorFlashes(ctx echo.Context) {
|
||||
sess, _ := store.Get(ctx.Request(), "flash")
|
||||
sess, _ := flashStore.Get(ctx.Request(), "flash")
|
||||
|
||||
setData(ctx, "flashErrors", sess.Flashes("error"))
|
||||
setData(ctx, "flashSuccess", sess.Flashes("success"))
|
||||
@@ -78,13 +77,13 @@ func setErrorFlashes(ctx echo.Context) {
|
||||
}
|
||||
|
||||
func addFlash(ctx echo.Context, flashMessage string, flashType string) {
|
||||
sess, _ := store.Get(ctx.Request(), "flash")
|
||||
sess, _ := flashStore.Get(ctx.Request(), "flash")
|
||||
sess.AddFlash(flashMessage, flashType)
|
||||
_ = sess.Save(ctx.Request(), ctx.Response())
|
||||
}
|
||||
|
||||
func getSession(ctx echo.Context) *sessions.Session {
|
||||
sess, _ := store.Get(ctx.Request(), "session")
|
||||
sess, _ := userStore.Get(ctx.Request(), "session")
|
||||
return sess
|
||||
}
|
||||
|
||||
@@ -110,7 +109,7 @@ func deleteCsrfCookie(ctx echo.Context) {
|
||||
}
|
||||
|
||||
func loadSettings(ctx echo.Context) error {
|
||||
settings, err := models.GetSettings()
|
||||
settings, err := db.GetSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,59 +122,6 @@ func loadSettings(ctx echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type OpengistValidator struct {
|
||||
v *validator.Validate
|
||||
}
|
||||
|
||||
func NewValidator() *OpengistValidator {
|
||||
v := validator.New()
|
||||
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
|
||||
return &OpengistValidator{v}
|
||||
}
|
||||
|
||||
func (cv *OpengistValidator) Validate(i interface{}) error {
|
||||
if err := cv.v.Struct(i); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validationMessages(err *error) string {
|
||||
errs := (*err).(validator.ValidationErrors)
|
||||
messages := make([]string, len(errs))
|
||||
for i, e := range errs {
|
||||
switch e.Tag() {
|
||||
case "max":
|
||||
messages[i] = e.Field() + " is too long"
|
||||
case "required":
|
||||
messages[i] = e.Field() + " should not be empty"
|
||||
case "excludes":
|
||||
messages[i] = e.Field() + " should not include a sub directory"
|
||||
case "alphanum":
|
||||
messages[i] = e.Field() + " should only contain alphanumeric characters"
|
||||
case "min":
|
||||
messages[i] = "Not enough " + e.Field()
|
||||
case "notreserved":
|
||||
messages[i] = "Invalid " + e.Field()
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(messages, " ; ")
|
||||
}
|
||||
|
||||
func validateReservedKeywords(fl validator.FieldLevel) bool {
|
||||
name := fl.Field().String()
|
||||
|
||||
restrictedNames := map[string]struct{}{}
|
||||
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search"} {
|
||||
restrictedNames[restrictedName] = struct{}{}
|
||||
}
|
||||
|
||||
// if the name is not in the restricted names, it is valid
|
||||
_, ok := restrictedNames[name]
|
||||
return !ok
|
||||
}
|
||||
|
||||
func getPage(ctx echo.Context) int {
|
||||
page := ctx.QueryParam("page")
|
||||
if page == "" {
|
||||
@@ -212,11 +158,11 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp
|
||||
|
||||
switch labels {
|
||||
case 1:
|
||||
setData(ctx, "prevLabel", "Previous")
|
||||
setData(ctx, "nextLabel", "Next")
|
||||
setData(ctx, "prevLabel", trH(ctx, "pagination.previous"))
|
||||
setData(ctx, "nextLabel", trH(ctx, "pagination.next"))
|
||||
case 2:
|
||||
setData(ctx, "prevLabel", "Newer")
|
||||
setData(ctx, "nextLabel", "Older")
|
||||
setData(ctx, "prevLabel", trH(ctx, "pagination.newer"))
|
||||
setData(ctx, "nextLabel", trH(ctx, "pagination.older"))
|
||||
}
|
||||
|
||||
setData(ctx, "urlPage", urlPage)
|
||||
@@ -224,59 +170,52 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp
|
||||
return nil
|
||||
}
|
||||
|
||||
type Argon2ID struct {
|
||||
format string
|
||||
version int
|
||||
time uint32
|
||||
memory uint32
|
||||
keyLen uint32
|
||||
saltLen uint32
|
||||
threads uint8
|
||||
func trH(ctx echo.Context, key string, args ...any) template.HTML {
|
||||
l := getData(ctx, "locale").(*i18n.Locale)
|
||||
return l.Tr(key, args...)
|
||||
}
|
||||
|
||||
var argon2id = Argon2ID{
|
||||
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
version: argon2.Version,
|
||||
time: 1,
|
||||
memory: 64 * 1024,
|
||||
keyLen: 32,
|
||||
saltLen: 16,
|
||||
threads: 4,
|
||||
func tr(ctx echo.Context, key string, args ...any) string {
|
||||
l := getData(ctx, "locale").(*i18n.Locale)
|
||||
return l.String(key, args...)
|
||||
}
|
||||
|
||||
func (a Argon2ID) hash(plain string) (string, error) {
|
||||
salt := make([]byte, a.saltLen)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
func parseSearchQueryStr(query string) (string, map[string]string) {
|
||||
words := strings.Fields(query)
|
||||
metadata := make(map[string]string)
|
||||
var contentBuilder strings.Builder
|
||||
|
||||
for _, word := range words {
|
||||
if strings.Contains(word, ":") {
|
||||
keyValue := strings.SplitN(word, ":", 2)
|
||||
if len(keyValue) == 2 {
|
||||
key := keyValue[0]
|
||||
value := keyValue[1]
|
||||
metadata[key] = value
|
||||
}
|
||||
} else {
|
||||
contentBuilder.WriteString(word + " ")
|
||||
}
|
||||
}
|
||||
|
||||
hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)
|
||||
|
||||
return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
|
||||
base64.RawStdEncoding.EncodeToString(salt),
|
||||
base64.RawStdEncoding.EncodeToString(hash),
|
||||
), nil
|
||||
content := strings.TrimSpace(contentBuilder.String())
|
||||
return content, metadata
|
||||
}
|
||||
|
||||
func (a Argon2ID) verify(plain, hash string) (bool, error) {
|
||||
hashParts := strings.Split(hash, "$")
|
||||
func addMetadataToSearchQuery(input, key, value string) string {
|
||||
content, metadata := parseSearchQueryStr(input)
|
||||
|
||||
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
|
||||
if err != nil {
|
||||
return false, err
|
||||
metadata[key] = value
|
||||
|
||||
var resultBuilder strings.Builder
|
||||
resultBuilder.WriteString(content)
|
||||
|
||||
for k, v := range metadata {
|
||||
resultBuilder.WriteString(" ")
|
||||
resultBuilder.WriteString(k)
|
||||
resultBuilder.WriteString(":")
|
||||
resultBuilder.WriteString(v)
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))
|
||||
|
||||
return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
|
||||
return strings.TrimSpace(resultBuilder.String())
|
||||
}
|
||||
|
||||
64
opengist.go
64
opengist.go
@@ -1,68 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"github.com/thomiceli/opengist/internal/models"
|
||||
"github.com/thomiceli/opengist/internal/ssh"
|
||||
"github.com/thomiceli/opengist/internal/web"
|
||||
"github.com/thomiceli/opengist/internal/cli"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func initialize() {
|
||||
fmt.Println("Opengist v" + config.OpengistVersion)
|
||||
|
||||
configPath := flag.String("config", "", "Path to a config file in YML format")
|
||||
flag.Parse()
|
||||
|
||||
if err := config.InitConfig(*configPath); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
config.InitLog()
|
||||
|
||||
gitVersion, err := git.GetGitVersion()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
if ok, err := config.CheckGitVersion(gitVersion); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
} else if !ok {
|
||||
log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.20. " +
|
||||
"Current git version: " + gitVersion)
|
||||
}
|
||||
|
||||
homePath := config.GetHomeDir()
|
||||
log.Info().Msg("Data directory: " + homePath)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "repos"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
|
||||
if err := models.Setup(filepath.Join(homePath, config.C.DBFilename)); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize database")
|
||||
}
|
||||
|
||||
web.EmbedFS = dirFS
|
||||
}
|
||||
|
||||
func main() {
|
||||
initialize()
|
||||
|
||||
go web.Start()
|
||||
go ssh.Start()
|
||||
|
||||
select {}
|
||||
if err := cli.App(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
6309
package-lock.json
generated
6309
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user