Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
24fc6dd8e4 | ||
|
|
cc6110bb4e | ||
|
|
2890c60124 | ||
|
|
5bb5886770 | ||
|
|
038d81df2d | ||
|
|
7515e82d34 | ||
|
|
add0299442 | ||
|
|
06b752f567 | ||
|
|
af3aab21e3 | ||
|
|
fb407bcbce |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +1,4 @@
|
|||||||
templates/**/* linguist-vendored
|
templates/**/* linguist-vendored
|
||||||
public/**/*.css 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
|
||||||
116
.github/workflows/go.yml
vendored
116
.github/workflows/go.yml
vendored
@@ -1,93 +1,63 @@
|
|||||||
name: "Go"
|
name: "Go CI"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 'dev-*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
checks:
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go 1.21
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "1.21"
|
||||||
|
|
||||||
|
- 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.21
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "1.21"
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
run: make go_mod check_changes
|
||||||
|
|
||||||
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: ["ubuntu-latest", "macOS-latest"]
|
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
|
||||||
go: ["1.19", "1.20"]
|
go: ["1.21"]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Go ${{ matrix.go }}
|
- name: Set up Go ${{ matrix.go }}
|
||||||
uses: WillAbides/setup-go-faster@v1.8.0
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Run tests
|
||||||
uses: actions/cache@v3
|
run: make test
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
docker-build-latest:
|
|
||||||
if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
||||||
needs:
|
|
||||||
- checks
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
ghcr.io/thomiceli/opengist
|
|
||||||
tags: |
|
|
||||||
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|||||||
@@ -1,11 +1,36 @@
|
|||||||
name: Docker
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
binaries-build-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go 1.21
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "1.21"
|
||||||
|
|
||||||
|
- 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:
|
docker-build-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ gist.db
|
|||||||
public/assets/*
|
public/assets/*
|
||||||
public/manifest.json
|
public/manifest.json
|
||||||
opengist
|
opengist
|
||||||
|
build/
|
||||||
|
|||||||
128
CHANGELOG.md
128
CHANGELOG.md
@@ -1,5 +1,133 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [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)
|
||||||
|
- Make unlisted gists not SEO crawlable (#78)
|
||||||
|
- Warning message on OAuth unlink (#79)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Redirect to `/all` when not logged in (#76)
|
||||||
|
- Removed Dev Docker image (#80)
|
||||||
|
|
||||||
|
## [1.4.1](https://github.com/thomiceli/opengist/compare/v1.4.0...v1.4.1) - 2023-06-25
|
||||||
|
### ⚠️ Docker users ⚠️
|
||||||
|
Opengist Docker volume has been changed from `/root/.opengist` to `/opengist`, do not forget to update your
|
||||||
|
`docker-compose.yml` file or any other Docker related configuration.
|
||||||
|
|
||||||
|
Please make a backup of your Opengist data directory before updating.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Git message remote: `warning: unable to access '/root/.config/git/attributes': Permission denied` (#71)
|
||||||
|
|
||||||
## [1.4.0](https://github.com/thomiceli/opengist/compare/v1.3.0...v1.4.0) - 2023-06-23
|
## [1.4.0](https://github.com/thomiceli/opengist/compare/v1.3.0...v1.4.0) - 2023-06-23
|
||||||
### ⚠️ Docker users ⚠️
|
### ⚠️ Docker users ⚠️
|
||||||
Opengist Docker volume has been changed from `/root/.opengist` to `/opengist`, do not forget to update your
|
Opengist Docker volume has been changed from `/root/.opengist` to `/opengist`, do not forget to update your
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.17 AS build
|
FROM alpine:3.19 AS build
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
@@ -7,10 +7,10 @@ RUN apk update && \
|
|||||||
musl-dev \
|
musl-dev \
|
||||||
libstdc++
|
libstdc++
|
||||||
|
|
||||||
COPY --from=golang:1.19-alpine /usr/local/go/ /usr/local/go/
|
COPY --from=golang:1.21-alpine /usr/local/go/ /usr/local/go/
|
||||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||||
|
|
||||||
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 NODE_PATH="/usr/local/lib/node_modules"
|
||||||
ENV PATH="/usr/local/bin:${PATH}"
|
ENV PATH="/usr/local/bin:${PATH}"
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ COPY . .
|
|||||||
RUN make
|
RUN make
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.17 as run
|
FROM alpine:3.19 as run
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
@@ -40,6 +40,8 @@ RUN apk update && \
|
|||||||
RUN addgroup -S opengist && \
|
RUN addgroup -S opengist && \
|
||||||
adduser -S -G opengist -H -s /bin/ash -g 'Opengist User' 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
|
WORKDIR /app/opengist
|
||||||
|
|
||||||
COPY --from=build --chown=opengist:opengist /opengist/opengist .
|
COPY --from=build --chown=opengist:opengist /opengist/opengist .
|
||||||
|
|||||||
32
Makefile
32
Makefile
@@ -1,9 +1,11 @@
|
|||||||
.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 watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test
|
||||||
|
|
||||||
# Specify the name of your Go binary output
|
# Specify the name of your Go binary output
|
||||||
BINARY_NAME := opengist
|
BINARY_NAME := opengist
|
||||||
|
|
||||||
all: install build
|
all: clean install build
|
||||||
|
|
||||||
|
all_crosscompile: clean install build_frontend build_crosscompile
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@echo "Installing NPM dependencies..."
|
@echo "Installing NPM dependencies..."
|
||||||
@@ -13,7 +15,8 @@ install:
|
|||||||
|
|
||||||
build_frontend:
|
build_frontend:
|
||||||
@echo "Building frontend assets..."
|
@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:
|
build_backend:
|
||||||
@echo "Building Opengist binary..."
|
@echo "Building Opengist binary..."
|
||||||
@@ -21,26 +24,43 @@ build_backend:
|
|||||||
|
|
||||||
build: build_frontend build_backend
|
build: build_frontend build_backend
|
||||||
|
|
||||||
|
build_crosscompile:
|
||||||
|
@bash ./scripts/build-all.sh
|
||||||
|
|
||||||
build_docker:
|
build_docker:
|
||||||
@echo "Building Docker image..."
|
@echo "Building Docker image..."
|
||||||
docker build -t $(BINARY_NAME):latest .
|
docker build -t $(BINARY_NAME):latest .
|
||||||
|
|
||||||
watch_frontend:
|
watch_frontend:
|
||||||
@echo "Building frontend assets..."
|
@echo "Building frontend assets..."
|
||||||
npx vite dev --port 16157
|
npx vite -c public/vite.config.js dev --port 16157
|
||||||
|
|
||||||
watch_backend:
|
watch_backend:
|
||||||
@echo "Building Opengist binary..."
|
@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 . --config config.yml'
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
@bash ./watch.sh
|
@bash ./scripts/watch.sh
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning up build artifacts..."
|
@echo "Cleaning up build artifacts..."
|
||||||
@rm -f $(BINARY_NAME) public/manifest.json
|
@rm -f $(BINARY_NAME) public/manifest.json
|
||||||
@rm -rf public/assets
|
@rm -rf public/assets build
|
||||||
|
|
||||||
clean_docker:
|
clean_docker:
|
||||||
@echo "Cleaning up Docker image..."
|
@echo "Cleaning up Docker image..."
|
||||||
@docker rmi $(BINARY_NAME)
|
@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
|
||||||
|
|||||||
230
README.md
230
README.md
@@ -1,66 +1,43 @@
|
|||||||
# Opengist
|
# 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) • [Demo](https://opengist.thomice.li)
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|
[](https://github.com/thomiceli/opengist/actions/workflows/go.yml)
|
||||||
[](https://goreportcard.com/report/github.com/thomiceli/opengist)
|
[](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
|
## Features
|
||||||
|
|
||||||
* Create public or unlisted snippets
|
* Create public, unlisted or private snippets
|
||||||
* Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
|
||||||
* Revisions history
|
|
||||||
* Syntax highlighting ; markdown & CSV support
|
* 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
|
* 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
|
* Download raw files or as a ZIP archive
|
||||||
* OAuth2 login with GitHub and Gitea
|
* 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
|
* Restrict or unrestrict snippets visibility to anonymous users
|
||||||
* Admin panel : delete users/gists; clean database/filesystem by syncing gists
|
|
||||||
* SQLite database
|
|
||||||
* Logging
|
|
||||||
* Docker support
|
* Docker support
|
||||||
|
* [More...](/docs/index.md#features)
|
||||||
|
|
||||||
#### Todo
|
## Quick start
|
||||||
|
|
||||||
- [ ] Translation
|
|
||||||
- [ ] Code/text search
|
|
||||||
- [ ] Embed snippets
|
|
||||||
- [ ] Tests
|
|
||||||
- [ ] Filesystem/Redis support for user sessions
|
|
||||||
- [ ] Have a cool logo
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
### With Docker
|
### With Docker
|
||||||
|
|
||||||
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release (`latest`)
|
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
|
||||||
and last development commit (`dev`) :
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker pull ghcr.io/thomiceli/opengist # most recent release
|
docker pull ghcr.io/thomiceli/opengist:1
|
||||||
|
|
||||||
docker pull ghcr.io/thomiceli/opengist:dev # latest development version
|
|
||||||
```
|
```
|
||||||
|
|
||||||
It can be used in a `docker-compose.yml` file :
|
It can be used in a `docker-compose.yml` file :
|
||||||
@@ -74,7 +51,7 @@ version: "3"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
opengist:
|
opengist:
|
||||||
image: ghcr.io/thomiceli/opengist:1.4
|
image: ghcr.io/thomiceli/opengist:1
|
||||||
container_name: opengist
|
container_name: opengist
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -95,9 +72,25 @@ services:
|
|||||||
GID: 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.6.0/opengist1.6.0-linux-amd64.tar.gz
|
||||||
|
|
||||||
|
tar xzvf opengist1.6.0-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
|
### 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.21+), [Node.js](https://nodejs.org/en/download/) (16+)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/thomiceli/opengist
|
git clone https://github.com/thomiceli/opengist
|
||||||
@@ -108,153 +101,12 @@ make
|
|||||||
|
|
||||||
Opengist is now running on port 6157, you can browse http://localhost:6157
|
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.
|
## Documentation
|
||||||
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.
|
|
||||||
|
|
||||||
<details>
|
The documentation is available in [/docs](/docs) directory.
|
||||||
<summary>Configuration option list</summary>
|
|
||||||
|
|
||||||
| 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. |
|
|
||||||
|
|
||||||
</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
|
## License
|
||||||
|
|
||||||
Opengist is licensed under the [AGPL-3.0 license](LICENSE).
|
Opengist is licensed under the [AGPL-3.0 license](/LICENSE).
|
||||||
|
|||||||
43
config.yml
43
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
|
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn
|
||||||
log-level: warn
|
log-level: warn
|
||||||
|
|
||||||
# Public URL for the Git HTTP/SSH connection.
|
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
|
||||||
# If not set, uses the URL from the request
|
log-output: stdout,file
|
||||||
|
|
||||||
|
# Public URL to access to Opengist
|
||||||
external-url:
|
external-url:
|
||||||
|
|
||||||
# Directory where Opengist will store its data. Default: ~/.opengist/
|
# Directory where Opengist will store its data. Default: ~/.opengist/
|
||||||
@@ -11,6 +17,16 @@ opengist-home:
|
|||||||
# Name of the SQLite database file. Default: opengist.db
|
# Name of the SQLite database file. Default: opengist.db
|
||||||
db-filename: 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
|
# Set the journal mode for SQLite. Default: WAL
|
||||||
# See https://www.sqlite.org/pragma.html#pragma_journal_mode
|
# See https://www.sqlite.org/pragma.html#pragma_journal_mode
|
||||||
sqlite.journal-mode: WAL
|
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
|
# Enable or disable git operations (clone, pull, push) via HTTP (either `true` or `false`). Default: true
|
||||||
http.git-enabled: 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
|
# SSH built-in server configuration
|
||||||
# Note: it is not using the SSH daemon from your machine (yet)
|
# Note: it is not using the SSH daemon from your machine (yet)
|
||||||
|
|
||||||
@@ -60,14 +67,26 @@ ssh.keygen-executable: ssh-keygen
|
|||||||
|
|
||||||
|
|
||||||
# OAuth2 configuration
|
# 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
|
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
|
||||||
github.client-key:
|
github.client-key:
|
||||||
github.secret:
|
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/
|
||||||
|
|
||||||
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
|
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
|
||||||
gitea.client-key:
|
gitea.client-key:
|
||||||
gitea.secret:
|
gitea.secret:
|
||||||
# URL of the Gitea instance. Default: https://gitea.com/
|
# URL of the Gitea instance. Default: https://gitea.com/
|
||||||
gitea.url: https://gitea.com/
|
gitea.url: https://gitea.com/
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ groupmod -o -g "$GID" $USER
|
|||||||
usermod -o -u "$UID" $USER
|
usermod -o -u "$UID" $USER
|
||||||
|
|
||||||
chown -R "$USER:$USER" /opengist
|
chown -R "$USER:$USER" /opengist
|
||||||
|
chown -R "$USER:$USER" /config.yml
|
||||||
|
|
||||||
export OG_OPENGIST_HOME=/opengist
|
exec su $USER -c "OG_OPENGIST_HOME=/opengist /app/opengist/opengist --config /config.yml"
|
||||||
|
|
||||||
su -m $USER -c "/app/opengist/opengist"
|
|
||||||
|
|||||||
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
|
||||||
|
```
|
||||||
47
docs/administration/run-with-systemd.md
Normal file
47
docs/administration/run-with-systemd.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Run with Systemd
|
||||||
|
|
||||||
|
For non-Docker users, you could run Opengist as a systemd service.
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
32
docs/configuration/cheat-sheet.md
Normal file
32
docs/configuration/cheat-sheet.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 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. |
|
||||||
|
| 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. |
|
||||||
|
| 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. |
|
||||||
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
|
||||||
|
```
|
||||||
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)
|
||||||
73
docs/installation.md
Normal file
73
docs/installation.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# 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.6.0/opengist1.6.0-linux-amd64.tar.gz
|
||||||
|
|
||||||
|
tar xzvf opengist1.6.0-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.21+)
|
||||||
|
* [Node.js](https://nodejs.org/en/download/) (16+)
|
||||||
|
|
||||||
|
```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.6.0/opengist1.6.0-linux-amd64.tar.gz
|
||||||
|
|
||||||
|
tar xzvf opengist1.6.0-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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
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(".")
|
|
||||||
100
go.mod
100
go.mod
@@ -1,42 +1,88 @@
|
|||||||
module github.com/thomiceli/opengist
|
module github.com/thomiceli/opengist
|
||||||
|
|
||||||
go 1.19
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-playground/validator/v10 v10.11.0
|
github.com/Kunde21/markdownfmt/v3 v3.1.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/alecthomas/chroma/v2 v2.12.0
|
||||||
github.com/gorilla/sessions v1.2.1
|
github.com/blevesearch/bleve/v2 v2.3.10
|
||||||
github.com/labstack/echo/v4 v4.10.0
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/markbates/goth v1.77.0
|
github.com/glebarez/go-sqlite v1.22.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.13
|
github.com/glebarez/sqlite v1.10.0
|
||||||
github.com/rs/zerolog v1.29.0
|
github.com/go-playground/validator/v10 v10.16.0
|
||||||
golang.org/x/crypto v0.2.0
|
github.com/google/uuid v1.5.0
|
||||||
golang.org/x/text v0.7.0
|
github.com/gorilla/sessions v1.2.2
|
||||||
|
github.com/hashicorp/go-memdb v1.3.4
|
||||||
|
github.com/labstack/echo/v4 v4.11.4
|
||||||
|
github.com/markbates/goth v1.78.0
|
||||||
|
github.com/rs/zerolog v1.31.0
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
github.com/yuin/goldmark v1.6.0
|
||||||
|
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.17.0
|
||||||
|
golang.org/x/text v0.14.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/sqlite v1.3.2
|
gorm.io/gorm v1.25.5
|
||||||
gorm.io/gorm v1.23.5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/RoaringBitmap/roaring v1.7.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||||
|
github.com/blevesearch/bleve_index_api v1.1.4 // indirect
|
||||||
|
github.com/blevesearch/geo v0.1.18 // 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.5 // 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/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // 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-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.4.2 // indirect
|
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
|
||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/gorilla/mux v1.6.2 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.2 // 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/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/labstack/gommon v0.4.0 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
golang.org/x/net v0.7.0 // indirect
|
go.etcd.io/bbolt v1.3.8 // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.5.0 // indirect
|
golang.org/x/oauth2 v0.15.0 // indirect
|
||||||
golang.org/x/time v0.2.0 // indirect
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
google.golang.org/appengine v1.6.6 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
google.golang.org/protobuf v1.25.0 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
|
modernc.org/libc v1.38.0 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
modernc.org/sqlite v1.28.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
293
go.sum
293
go.sum
@@ -34,39 +34,111 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
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/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/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
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.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
|
||||||
|
github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||||
|
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||||
|
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
|
||||||
|
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||||
|
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||||
|
github.com/alecthomas/repr v0.2.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.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg=
|
||||||
|
github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA=
|
||||||
|
github.com/blevesearch/bleve_index_api v1.1.4 h1:n9Ilxlb80g9DAhchR95IcVrzohamDSri0wPnkKnva50=
|
||||||
|
github.com/blevesearch/bleve_index_api v1.1.4/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
|
||||||
|
github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw=
|
||||||
|
github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
|
||||||
|
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.5 h1:5SsNQmR8v1bojtGQ1zFhZravcMg5rdiX8AVu6LwlVtc=
|
||||||
|
github.com/blevesearch/scorch_segment_api/v2 v2.2.5/go.mod h1:8N2ytOlBCdurlxDgbqsfeR1oTKRN0ZVIKdUUP1VFZNc=
|
||||||
|
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
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/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/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/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/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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/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/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
||||||
|
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.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
|
github.com/dlclark/regexp2 v1.10.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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
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.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/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/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
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.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
|
||||||
|
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
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-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-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.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
|
||||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
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/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
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 h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
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-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
|
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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
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-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-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@@ -90,8 +162,13 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
|||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
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.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
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/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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@@ -101,8 +178,13 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.4.1/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.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.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/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
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/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-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
@@ -113,96 +195,136 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
|
|||||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/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-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
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.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.5.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.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
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/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/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
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/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
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.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
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.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
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.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/hashicorp/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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
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/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
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/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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
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/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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||||
github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ=
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
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/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/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/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/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/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
|
||||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
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.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
|
||||||
github.com/markbates/goth v1.77.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
|
github.com/markbates/goth v1.78.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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
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-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.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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
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/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
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/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||||
|
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
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.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.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.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
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/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 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
|
||||||
|
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
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.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||||
|
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
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.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
@@ -213,10 +335,10 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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-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-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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
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-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-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -247,6 +369,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -274,16 +397,19 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
|
|||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/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-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-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-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-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-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-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/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||||
|
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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-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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -292,6 +418,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-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/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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-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-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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -321,15 +448,17 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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-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-20220520151302-bc2c85ada10a/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-20220722155257-8c9f86f7a55f/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=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -337,13 +466,14 @@ 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
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-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.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.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
@@ -386,10 +516,10 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
|
|||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/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-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||||
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-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-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=
|
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.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.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
@@ -413,8 +543,9 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
|||||||
google.golang.org/appengine v1.5.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.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
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/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||||
|
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-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-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
@@ -469,23 +600,21 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
|||||||
google.golang.org/protobuf v1.23.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.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.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=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
|
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/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.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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||||
gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
|
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
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-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-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-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
@@ -493,6 +622,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
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.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
modernc.org/libc v1.38.0 h1:o4Lpk0zNDSdsjfEXnF1FGXWQ9PDi1NOdWcLP5n13FGo=
|
||||||
|
modernc.org/libc v1.38.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||||
|
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.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||||
|
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||||
|
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||||
|
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|||||||
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 = iota
|
||||||
|
GitGcRepos = iota
|
||||||
|
SyncGistPreviews = iota
|
||||||
|
ResetHooks = iota
|
||||||
|
IndexGists = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
panic("unhandled default case")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,12 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -14,7 +17,7 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var OpengistVersion = "1.4.0"
|
var OpengistVersion = "1.6.0"
|
||||||
|
|
||||||
var C *config
|
var C *config
|
||||||
|
|
||||||
@@ -22,18 +25,20 @@ var C *config
|
|||||||
// doesn't support dot notation in this case sadly
|
// doesn't support dot notation in this case sadly
|
||||||
type config struct {
|
type config struct {
|
||||||
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
|
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"`
|
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
|
||||||
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
|
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
|
||||||
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"`
|
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"`
|
SqliteJournalMode string `yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE"`
|
||||||
|
|
||||||
HttpHost string `yaml:"http.host" env:"OG_HTTP_HOST"`
|
HttpHost string `yaml:"http.host" env:"OG_HTTP_HOST"`
|
||||||
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
|
HttpPort string `yaml:"http.port" env:"OG_HTTP_PORT"`
|
||||||
HttpGit bool `yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED"`
|
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"`
|
|
||||||
|
|
||||||
SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"`
|
SshGit bool `yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED"`
|
||||||
SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"`
|
SshHost string `yaml:"ssh.host" env:"OG_SSH_HOST"`
|
||||||
@@ -44,35 +49,41 @@ type config struct {
|
|||||||
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
|
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
|
||||||
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
|
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"`
|
||||||
|
|
||||||
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
|
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
|
||||||
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
|
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
|
||||||
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
|
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
|
||||||
|
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func configWithDefaults() (*config, error) {
|
func configWithDefaults() (*config, error) {
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
c := &config{}
|
c := &config{}
|
||||||
if err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.LogLevel = "warn"
|
c.LogLevel = "warn"
|
||||||
c.OpengistHome = filepath.Join(homeDir, ".opengist")
|
c.LogOutput = "stdout,file"
|
||||||
|
c.OpengistHome = ""
|
||||||
c.DBFilename = "opengist.db"
|
c.DBFilename = "opengist.db"
|
||||||
|
c.IndexEnabled = true
|
||||||
|
c.IndexDirname = "opengist.index"
|
||||||
|
|
||||||
c.SqliteJournalMode = "WAL"
|
c.SqliteJournalMode = "WAL"
|
||||||
|
|
||||||
c.HttpHost = "0.0.0.0"
|
c.HttpHost = "0.0.0.0"
|
||||||
c.HttpPort = "6157"
|
c.HttpPort = "6157"
|
||||||
c.HttpGit = true
|
c.HttpGit = true
|
||||||
c.HttpTLSEnabled = false
|
|
||||||
|
|
||||||
c.SshGit = true
|
c.SshGit = true
|
||||||
c.SshHost = "0.0.0.0"
|
c.SshHost = "0.0.0.0"
|
||||||
c.SshPort = "2222"
|
c.SshPort = "2222"
|
||||||
c.SshKeygen = "ssh-keygen"
|
c.SshKeygen = "ssh-keygen"
|
||||||
|
|
||||||
c.GiteaUrl = "http://gitea.com"
|
c.GiteaUrl = "https://gitea.com"
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
@@ -92,6 +103,19 @@ func InitConfig(configPath string) error {
|
|||||||
return err
|
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
|
C = c
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -101,21 +125,46 @@ func InitLog() {
|
|||||||
if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil {
|
||||||
panic(err)
|
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
|
var level zerolog.Level
|
||||||
level, err = zerolog.ParseLevel(C.LogLevel)
|
level, err := zerolog.ParseLevel(C.LogLevel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
level = zerolog.InfoLevel
|
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()
|
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)
|
log.Warn().Msg("Invalid log level: " + C.LogLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,8 +183,8 @@ func CheckGitVersion(version string) (bool, error) {
|
|||||||
return false, fmt.Errorf("invalid minor version number")
|
return false, fmt.Errorf("invalid minor version number")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if version is prior to 2.20
|
// Check if version is prior to 2.28
|
||||||
if major < 2 || (major == 2 && minor < 20) {
|
if major < 2 || (major == 2 && minor < 28) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -170,17 +219,6 @@ func loadConfigFromYaml(c *config, configPath string) error {
|
|||||||
fmt.Println("No YAML config file specified.")
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,3 +260,19 @@ func loadConfigFromEnv(c *config) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checks(c *config) error {
|
||||||
|
if _, err := url.Parse(c.ExternalUrl); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := url.Parse(c.GiteaUrl); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := url.Parse(c.OIDCDiscoveryUrl); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package models
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
@@ -1,28 +1,34 @@
|
|||||||
package models
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"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/rs/zerolog/log"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/utils"
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
|
|
||||||
func Setup(dbPath string) error {
|
func Setup(dbPath string, sharedCache bool) error {
|
||||||
var err error
|
var err error
|
||||||
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
|
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)
|
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),
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -40,7 +46,9 @@ func Setup(dbPath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyMigrations(db)
|
if err = ApplyMigrations(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Default admin setting values
|
// Default admin setting values
|
||||||
return initAdminSettings(map[string]string{
|
return initAdminSettings(map[string]string{
|
||||||
@@ -51,6 +59,14 @@ func Setup(dbPath string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Close() error {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sqlDB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func CountAll(table interface{}) (int64, error) {
|
func CountAll(table interface{}) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
err := db.Model(table).Count(&count).Error
|
err := db.Model(table).Count(&count).Error
|
||||||
@@ -58,9 +74,18 @@ func CountAll(table interface{}) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IsUniqueConstraintViolation(err error) bool {
|
func IsUniqueConstraintViolation(err error) bool {
|
||||||
var sqliteErr sqlite3.Error
|
var sqliteErr *msqlite.Error
|
||||||
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
if errors.As(err, &sqliteErr) && sqliteErr.Code() == 2067 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Ping() error {
|
||||||
|
sql, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql.Ping()
|
||||||
|
}
|
||||||
@@ -1,21 +1,63 @@
|
|||||||
package models
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
"fmt"
|
||||||
"gorm.io/gorm"
|
"github.com/alecthomas/chroma/v2"
|
||||||
|
"github.com/alecthomas/chroma/v2/lexers"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/index"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Visibility int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PublicVisibility Visibility = iota
|
||||||
|
UnlistedVisibility
|
||||||
|
PrivateVisibility
|
||||||
|
)
|
||||||
|
|
||||||
|
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":
|
||||||
|
return PublicVisibility, nil
|
||||||
|
case "1":
|
||||||
|
return UnlistedVisibility, nil
|
||||||
|
case "2":
|
||||||
|
return PrivateVisibility, nil
|
||||||
|
default:
|
||||||
|
return -1, fmt.Errorf("unknown visibility %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Gist struct {
|
type Gist struct {
|
||||||
ID uint `gorm:"primaryKey"`
|
ID uint `gorm:"primaryKey"`
|
||||||
Uuid string
|
Uuid string
|
||||||
Title string
|
Title string
|
||||||
|
URL string
|
||||||
Preview string
|
Preview string
|
||||||
PreviewFilename string
|
PreviewFilename string
|
||||||
Description string
|
Description string
|
||||||
Private bool
|
Private Visibility // 0: public, 1: unlisted, 2: private
|
||||||
UserID uint
|
UserID uint
|
||||||
User User
|
User User
|
||||||
NbFiles int
|
NbFiles int
|
||||||
@@ -47,7 +89,7 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
|||||||
func GetGist(user string, gistUuid string) (*Gist, error) {
|
func GetGist(user string, gistUuid string) (*Gist, error) {
|
||||||
gist := new(Gist)
|
gist := new(Gist)
|
||||||
err := db.Preload("User").Preload("Forked.User").
|
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").
|
Joins("join users on gists.user_id = users.id").
|
||||||
First(&gist).Error
|
First(&gist).Error
|
||||||
|
|
||||||
@@ -89,7 +131,7 @@ func GetAllGists(offset int) ([]*Gist, error) {
|
|||||||
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) {
|
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) {
|
||||||
var gists []*Gist
|
var gists []*Gist
|
||||||
err := db.Preload("User").Preload("Forked.User").
|
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+"%").
|
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%").
|
||||||
Limit(11).
|
Limit(11).
|
||||||
Offset(offset * 10).
|
Offset(offset * 10).
|
||||||
@@ -101,7 +143,7 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st
|
|||||||
|
|
||||||
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||||
return db.Preload("User").Preload("Forked.User").
|
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).
|
Where("users.id = ?", fromUserId).
|
||||||
Joins("join users on gists.user_id = users.id")
|
Joins("join users on gists.user_id = users.id")
|
||||||
}
|
}
|
||||||
@@ -124,7 +166,7 @@ func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
|
|||||||
|
|
||||||
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||||
return db.Preload("User").Preload("Forked.User").
|
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).
|
Where("likes.user_id = ?", fromUserId).
|
||||||
Joins("join likes on gists.id = likes.gist_id").
|
Joins("join likes on gists.id = likes.gist_id").
|
||||||
Joins("join users on likes.user_id = users.id")
|
Joins("join users on likes.user_id = users.id")
|
||||||
@@ -147,7 +189,7 @@ func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error
|
|||||||
|
|
||||||
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
|
||||||
return db.Preload("User").Preload("Forked.User").
|
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).
|
Where("gists.user_id = ?", fromUserId).
|
||||||
Joins("join users on gists.user_id = users.id")
|
Joins("join users on gists.user_id = users.id")
|
||||||
}
|
}
|
||||||
@@ -176,6 +218,25 @@ func GetAllGistsRows() ([]*Gist, error) {
|
|||||||
return gists, err
|
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 {
|
func (gist *Gist) Create() error {
|
||||||
// avoids foreign key constraint error because the default value in the struct is 0
|
// avoids foreign key constraint error because the default value in the struct is 0
|
||||||
return db.Omit("forked_id").Create(&gist).Error
|
return db.Omit("forked_id").Create(&gist).Error
|
||||||
@@ -189,7 +250,16 @@ func (gist *Gist) Update() error {
|
|||||||
return db.Omit("forked_id").Save(&gist).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 {
|
func (gist *Gist) Delete() error {
|
||||||
|
err := gist.DeleteRepository()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return db.Delete(&gist).Error
|
return db.Delete(&gist).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +313,7 @@ func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) {
|
|||||||
var gists []*Gist
|
var gists []*Gist
|
||||||
err := db.Model(&gist).Preload("User").
|
err := db.Model(&gist).Preload("User").
|
||||||
Where("forked_id = ?", gist.ID).
|
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).
|
Limit(11).
|
||||||
Offset(offset * 10).
|
Offset(offset * 10).
|
||||||
Order("updated_at desc").
|
Order("updated_at desc").
|
||||||
@@ -260,29 +330,33 @@ func (gist *Gist) InitRepository() error {
|
|||||||
return git.InitRepository(gist.User.Username, gist.Uuid)
|
return git.InitRepository(gist.User.Username, gist.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gist *Gist) InitRepositoryViaInit(ctx echo.Context) error {
|
||||||
|
return git.InitRepositoryViaInit(gist.User.Username, gist.Uuid, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func (gist *Gist) DeleteRepository() error {
|
func (gist *Gist) DeleteRepository() error {
|
||||||
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) Files(revision string) ([]*git.File, error) {
|
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
|
||||||
var files []*git.File
|
filesCat, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
|
||||||
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if the revision or the file do not exist
|
// if the revision or the file do not exist
|
||||||
|
|
||||||
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
|
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
|
||||||
return nil, nil
|
return nil, &git.RevisionNotFoundError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fileStr := range filesStr {
|
var files []*git.File
|
||||||
file, err := gist.File(revision, fileStr, true)
|
for _, fileCat := range filesCat {
|
||||||
if err != nil {
|
files = append(files, &git.File{
|
||||||
return nil, err
|
Filename: fileCat.Name,
|
||||||
}
|
Size: fileCat.Size,
|
||||||
files = append(files, file)
|
HumanSize: humanize.IBytes(fileCat.Size),
|
||||||
|
Content: fileCat.Content,
|
||||||
|
Truncated: fileCat.Truncated,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return files, err
|
return files, err
|
||||||
}
|
}
|
||||||
@@ -295,23 +369,36 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
|
|||||||
return nil, nil
|
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{
|
return &git.File{
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
|
Size: size,
|
||||||
|
HumanSize: humanize.IBytes(size),
|
||||||
Content: content,
|
Content: content,
|
||||||
Truncated: truncated,
|
Truncated: truncated,
|
||||||
}, err
|
}, 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) {
|
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
|
||||||
return git.GetLog(gist.User.Username, gist.Uuid, skip)
|
return git.GetLog(gist.User.Username, gist.Uuid, skip)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) NbCommits() (string, error) {
|
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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,6 +419,26 @@ func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
|
|||||||
return git.Push(gist.Uuid)
|
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 {
|
func (gist *Gist) ForkClone(username string, uuid string) error {
|
||||||
return git.ForkClone(gist.User.Username, gist.Uuid, username, uuid)
|
return git.ForkClone(gist.User.Username, gist.Uuid, username, uuid)
|
||||||
}
|
}
|
||||||
@@ -344,7 +451,7 @@ func (gist *Gist) RPC(service string) ([]byte, error) {
|
|||||||
return git.RPC(gist.User.Username, gist.Uuid, service)
|
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")
|
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -367,24 +474,73 @@ func (gist *Gist) UpdatePreviewAndCount() error {
|
|||||||
gist.Preview = file.Content
|
gist.Preview = file.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
gist.Preview = file.Content
|
|
||||||
gist.PreviewFilename = file.Filename
|
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 -- //
|
// -- DTO -- //
|
||||||
|
|
||||||
type GistDTO struct {
|
type GistDTO struct {
|
||||||
Title string `validate:"max=50" form:"title"`
|
Title string `validate:"max=250" form:"title"`
|
||||||
Description string `validate:"max=150" form:"description"`
|
Description string `validate:"max=1000" form:"description"`
|
||||||
Private bool `form:"private"`
|
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
|
||||||
Files []FileDTO `validate:"min=1,dive"`
|
Private Visibility `validate:"number,min=0,max=2" form:"private"`
|
||||||
|
Files []FileDTO `validate:"min=1,dive"`
|
||||||
|
Name []string `form:"name"`
|
||||||
|
Content []string `form:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileDTO struct {
|
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"`
|
Content string `validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,12 +549,84 @@ func (dto *GistDTO) ToGist() *Gist {
|
|||||||
Title: dto.Title,
|
Title: dto.Title,
|
||||||
Description: dto.Description,
|
Description: dto.Description,
|
||||||
Private: dto.Private,
|
Private: dto.Private,
|
||||||
|
URL: dto.URL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
|
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
|
||||||
gist.Title = dto.Title
|
gist.Title = dto.Title
|
||||||
gist.Description = dto.Description
|
gist.Description = dto.Description
|
||||||
gist.Private = dto.Private
|
gist.URL = dto.URL
|
||||||
return gist
|
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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package models
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package models
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package models
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -14,7 +14,9 @@ type User struct {
|
|||||||
MD5Hash string // for gravatar, if no Email is specified, the value is random
|
MD5Hash string // for gravatar, if no Email is specified, the value is random
|
||||||
AvatarURL string
|
AvatarURL string
|
||||||
GithubID string
|
GithubID string
|
||||||
|
GitlabID string
|
||||||
GiteaID string
|
GiteaID string
|
||||||
|
OIDCID string `gorm:"column:oidc_id"`
|
||||||
|
|
||||||
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||||
SSHKeys []SSHKey `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
|
// Decrement forks counter for all gists forked by this user
|
||||||
return tx.Model(&Gist{}).
|
err = tx.Model(&Gist{}).
|
||||||
Omit("updated_at").
|
Omit("updated_at").
|
||||||
Where("id IN (?)", tx.
|
Where("id IN (?)", tx.
|
||||||
Select("forked_id").
|
Select("forked_id").
|
||||||
@@ -47,6 +49,17 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
|
|||||||
).
|
).
|
||||||
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")).
|
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")).
|
||||||
Error
|
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) {
|
func UserExists(username string) (bool, error) {
|
||||||
@@ -93,7 +106,6 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
|
|||||||
err := db.
|
err := db.
|
||||||
Where("email IN ?", emails).
|
Where("email IN ?", emails).
|
||||||
Find(&users).Error
|
Find(&users).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -122,8 +134,12 @@ func GetUserByProvider(id string, provider string) (*User, error) {
|
|||||||
switch provider {
|
switch provider {
|
||||||
case "github":
|
case "github":
|
||||||
err = db.Where("github_id = ?", id).First(&user).Error
|
err = db.Where("github_id = ?", id).First(&user).Error
|
||||||
|
case "gitlab":
|
||||||
|
err = db.Where("gitlab_id = ?", id).First(&user).Error
|
||||||
case "gitea":
|
case "gitea":
|
||||||
err = db.Where("gitea_id = ?", id).First(&user).Error
|
err = db.Where("gitea_id = ?", id).First(&user).Error
|
||||||
|
case "openid-connect":
|
||||||
|
err = db.Where("oidc_id = ?", id).First(&user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, err
|
return user, err
|
||||||
@@ -158,15 +174,16 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) DeleteProviderID(provider string) error {
|
func (user *User) DeleteProviderID(provider string) error {
|
||||||
switch provider {
|
providerIDFields := map[string]string{
|
||||||
case "github":
|
"github": "github_id",
|
||||||
|
"gitlab": "gitlab_id",
|
||||||
|
"gitea": "gitea_id",
|
||||||
|
"openid-connect": "oidc_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
if providerIDField, ok := providerIDFields[provider]; ok {
|
||||||
return db.Model(&user).
|
return db.Model(&user).
|
||||||
Update("github_id", nil).
|
Update(providerIDField, nil).
|
||||||
Update("avatar_url", nil).
|
|
||||||
Error
|
|
||||||
case "gitea":
|
|
||||||
return db.Model(&user).
|
|
||||||
Update("gitea_id", nil).
|
|
||||||
Update("avatar_url", nil).
|
Update("avatar_url", nil).
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
@@ -177,7 +194,7 @@ func (user *User) DeleteProviderID(provider string) error {
|
|||||||
// -- DTO -- //
|
// -- DTO -- //
|
||||||
|
|
||||||
type UserDTO struct {
|
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"`
|
Password string `form:"password" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,18 +1,55 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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
|
||||||
|
|
||||||
|
type RevisionNotFoundError struct{}
|
||||||
|
|
||||||
|
func (m *RevisionNotFoundError) Error() string {
|
||||||
|
return "revision not found"
|
||||||
|
}
|
||||||
|
|
||||||
func RepositoryPath(user string, gist string) string {
|
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 {
|
func TmpRepositoryPath(gistId string) string {
|
||||||
@@ -27,22 +64,33 @@ func TmpRepositoriesPath() string {
|
|||||||
func InitRepository(user string, gist string) error {
|
func InitRepository(user string, gist string) error {
|
||||||
repositoryPath := RepositoryPath(user, gist)
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
cmd := exec.Command(
|
var args []string
|
||||||
"git",
|
args = append(args, "init")
|
||||||
"init",
|
if config.C.GitDefaultBranch != "" {
|
||||||
"--bare",
|
args = append(args, "--initial-branch", config.C.GitDefaultBranch)
|
||||||
repositoryPath,
|
}
|
||||||
)
|
args = append(args, "--bare", repositoryPath)
|
||||||
|
|
||||||
err := cmd.Run()
|
cmd := exec.Command("git", args...)
|
||||||
if err != nil {
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return copyFiles(repositoryPath)
|
return CreateDotGitFiles(user, gist)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetNumberOfCommitsOfRepository(user string, gist string) (string, error) {
|
func InitRepositoryViaInit(user string, gist string, ctx echo.Context) error {
|
||||||
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
|
if err := InitRepository(user, gist); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
repositoryUrl := RepositoryUrl(ctx, user, gist)
|
||||||
|
return createDotGitHookFile(repositoryPath, "post-receive", fmt.Sprintf(postReceive, repositoryUrl, repositoryUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
func CountCommits(user string, gist string) (string, error) {
|
||||||
repositoryPath := RepositoryPath(user, gist)
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
@@ -78,15 +126,134 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
|
|||||||
return slice[:len(slice)-1], nil
|
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) {
|
func GetFileContent(user string, gist string, revision string, filename string, truncate bool) (string, bool, error) {
|
||||||
repositoryPath := RepositoryPath(user, gist)
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
var maxBytes int64 = -1
|
var maxBytes int64 = -1
|
||||||
if truncate {
|
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",
|
"git",
|
||||||
"--no-pager",
|
"--no-pager",
|
||||||
"show",
|
"show",
|
||||||
@@ -94,14 +261,36 @@ func GetFileContent(user string, gist string, revision string, filename string,
|
|||||||
)
|
)
|
||||||
cmd.Dir = repositoryPath
|
cmd.Dir = repositoryPath
|
||||||
|
|
||||||
stdout, _ := cmd.StdoutPipe()
|
output, err := cmd.Output()
|
||||||
err := cmd.Start()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
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) {
|
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
||||||
@@ -127,12 +316,17 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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, truncateLimit), err
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
tmpPath := TmpRepositoriesPath()
|
tmpPath := TmpRepositoriesPath()
|
||||||
@@ -150,13 +344,13 @@ func CloneTmp(user string, gist string, gistTmpId string, email string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove every file (and not the .git directory!)
|
// remove every file (keep the .git directory)
|
||||||
cmd = exec.Command("find", ".", "-maxdepth", "1", "-type", "f", "-delete")
|
// useful when user wants to edit multiple files from an existing gist
|
||||||
cmd.Dir = tmpRepositoryPath
|
if remove {
|
||||||
if err = cmd.Run(); err != nil {
|
if err = removeFilesExceptGit(tmpRepositoryPath); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd = exec.Command("git", "config", "--local", "user.name", user)
|
cmd = exec.Command("git", "config", "--local", "user.name", user)
|
||||||
cmd.Dir = tmpRepositoryPath
|
cmd.Dir = tmpRepositoryPath
|
||||||
if err = cmd.Run(); err != nil {
|
if err = cmd.Run(); err != nil {
|
||||||
@@ -177,7 +371,7 @@ func ForkClone(userSrc string, gistSrc string, userDst string, gistDst string) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return copyFiles(repositoryPathDst)
|
return CreateDotGitFiles(userDst, gistDst)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetFileContent(gistTmpId string, filename string, content string) error {
|
func SetFileContent(gistTmpId string, filename string, content string) error {
|
||||||
@@ -255,6 +449,67 @@ func RPC(user string, gist string, service string) ([]byte, error) {
|
|||||||
return stdout, err
|
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) {
|
func GetGitVersion() (string, error) {
|
||||||
cmd := exec.Command("git", "--version")
|
cmd := exec.Command("git", "--version")
|
||||||
stdout, err := cmd.Output()
|
stdout, err := cmd.Output()
|
||||||
@@ -270,19 +525,29 @@ func GetGitVersion() (string, error) {
|
|||||||
return versionFields[2], nil
|
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)
|
f1, err := os.OpenFile(filepath.Join(repositoryPath, "git-daemon-export-ok"), os.O_RDONLY|os.O_CREATE, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f1.Close()
|
defer f1.Close()
|
||||||
|
|
||||||
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", "pre-receive"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744)
|
if err = createDotGitHookFile(repositoryPath, "pre-receive", preReceive); 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = preReceiveDst.WriteString(preReceive); err != nil {
|
if _, err = preReceiveDst.WriteString(content); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer preReceiveDst.Close()
|
defer preReceiveDst.Close()
|
||||||
@@ -290,12 +555,35 @@ func copyFiles(repositoryPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const preReceive = `#!/bin/sh
|
const preReceive = `#!/bin/sh
|
||||||
|
|
||||||
disallowed_files=""
|
disallowed_files=""
|
||||||
|
|
||||||
while read -r old_rev new_rev ref
|
while read -r old_rev new_rev ref
|
||||||
do
|
do
|
||||||
|
if [ "$old_rev" = "0000000000000000000000000000000000000000" ]; then
|
||||||
|
# This is the first commit, so we check all the files in that commit
|
||||||
|
changed_files=$(git ls-tree -r --name-only "$new_rev")
|
||||||
|
else
|
||||||
|
# This is not the first commit, so we compare it with its predecessor
|
||||||
|
changed_files=$(git diff --name-only "$old_rev" "$new_rev")
|
||||||
|
fi
|
||||||
|
|
||||||
while IFS= read -r file
|
while IFS= read -r file
|
||||||
do
|
do
|
||||||
case $file in
|
case $file in
|
||||||
@@ -304,15 +592,35 @@ do
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done <<EOF
|
done <<EOF
|
||||||
$(git diff --name-only "$old_rev" "$new_rev")
|
$changed_files
|
||||||
EOF
|
EOF
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -n "$disallowed_files" ]; then
|
if [ -n "$disallowed_files" ]; then
|
||||||
|
echo ""
|
||||||
echo "Pushing files in folders is not allowed:"
|
echo "Pushing files in folders is not allowed:"
|
||||||
for file in $disallowed_files; do
|
for file in $disallowed_files; do
|
||||||
echo " $file"
|
echo " $file"
|
||||||
done
|
done
|
||||||
|
echo ""
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const postReceive = `#!/bin/sh
|
||||||
|
|
||||||
|
while read oldrev newrev refname; do
|
||||||
|
if ! git rev-parse --verify --quiet HEAD &>/dev/null; then
|
||||||
|
git symbolic-ref HEAD "$refname"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Your new repository has been created here: %s"
|
||||||
|
echo ""
|
||||||
|
echo "If you want to keep working with your gist, you could set the remote URL via:"
|
||||||
|
echo "git remote set-url origin %s"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
rm -f $0
|
||||||
|
`
|
||||||
|
|||||||
319
internal/git/commands_test.go
Normal file
319
internal/git/commands_test.go
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setup(t *testing.T) {
|
||||||
|
err := config.InitConfig("")
|
||||||
|
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 teardown(t *testing.T) {
|
||||||
|
err := os.RemoveAll(path.Join(config.C.OpengistHome, "tests"))
|
||||||
|
require.NoError(t, err, "Could not remove repos directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitDeleteRepository(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
defer teardown(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"), "hooks", "pre-receive"))
|
||||||
|
require.NoError(t, err, "pre-receive hook not found")
|
||||||
|
|
||||||
|
_, 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) {
|
||||||
|
setup(t)
|
||||||
|
defer teardown(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) {
|
||||||
|
setup(t)
|
||||||
|
defer teardown(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: "",
|
||||||
|
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) {
|
||||||
|
setup(t)
|
||||||
|
defer teardown(t)
|
||||||
|
|
||||||
|
err := GcRepos()
|
||||||
|
require.NoError(t, err, "Could not run git gc")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFork(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
defer teardown(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) {
|
||||||
|
setup(t)
|
||||||
|
defer teardown(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 TestInitViaGitInit(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
defer teardown(t)
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
|
||||||
|
// Create a mock HTTP request
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||||
|
|
||||||
|
// Create a mock HTTP response recorder
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Create a new Echo context
|
||||||
|
c := e.NewContext(req, rec)
|
||||||
|
|
||||||
|
// Define your user and gist
|
||||||
|
user := "testUser"
|
||||||
|
gist := "testGist"
|
||||||
|
|
||||||
|
err := InitRepositoryViaInit(user, gist, c)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitInitBranchNames(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
defer teardown(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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 commit to repository")
|
||||||
|
|
||||||
|
if len(files) > 0 {
|
||||||
|
for filename, content := range files {
|
||||||
|
if err := SetFileContent(gist, filename, content); err != nil {
|
||||||
|
require.NoError(t, err, "Could not commit to repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := AddAll(gist); err != nil {
|
||||||
|
require.NoError(t, err, "Could not commit 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 commit to repository")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,12 +11,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
Filename string
|
Filename string `json:"filename"`
|
||||||
OldFilename string
|
Size uint64 `json:"size"`
|
||||||
Content string
|
HumanSize string `json:"human_size"`
|
||||||
Truncated bool
|
OldFilename string `json:"-"`
|
||||||
IsCreated bool
|
Content string `json:"content"`
|
||||||
IsDeleted bool
|
Truncated bool `json:"truncated"`
|
||||||
|
IsCreated bool `json:"-"`
|
||||||
|
IsDeleted bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CsvFile struct {
|
type CsvFile struct {
|
||||||
@@ -92,6 +94,11 @@ func parseLog(out io.Reader, maxBytes int) []*Commit {
|
|||||||
|
|
||||||
scanner.Scan()
|
scanner.Scan()
|
||||||
|
|
||||||
|
if len(scanner.Bytes()) == 0 {
|
||||||
|
commits = append(commits, currentCommit)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// if there is no shortstat, it means that the commit is empty, we add it and move onto the next one
|
// 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] != ' ' {
|
if scanner.Bytes()[0] != ' ' {
|
||||||
commits = append(commits, currentCommit)
|
commits = append(commits, currentCommit)
|
||||||
|
|||||||
127
internal/i18n/locale.go
Normal file
127
internal/i18n/locale.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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) 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...))
|
||||||
|
}
|
||||||
184
internal/i18n/locales/cs-CZ.yml
Normal file
184
internal/i18n/locales/cs-CZ.yml
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
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.github-oauth: Pokračovat s účtem na GitHubu
|
||||||
|
auth.gitea-oauth: Pokračovat s účtem na Gitea
|
||||||
|
|
||||||
|
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.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?
|
||||||
198
internal/i18n/locales/en-US.yml
Normal file
198
internal/i18n/locales/en-US.yml
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
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.edit.editing: Editing
|
||||||
|
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.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.likes: Likes
|
||||||
|
gist.likes.no: No likes yet
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.github-oauth: Continue with GitHub account
|
||||||
|
auth.gitlab-oauth: Continue with GitLab account
|
||||||
|
auth.gitea-oauth: Continue with Gitea account
|
||||||
|
|
||||||
|
error: Error
|
||||||
|
|
||||||
|
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.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.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 ?
|
||||||
177
internal/i18n/locales/es-ES.yml
Normal file
177
internal/i18n/locales/es-ES.yml
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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.github-oauth: Continuar con cuenta de GitHub
|
||||||
|
auth.gitea-oauth: Continuar con cuenta de Gitea
|
||||||
|
|
||||||
|
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.config-link-overridden: sobrescrito
|
||||||
|
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.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?
|
||||||
177
internal/i18n/locales/fr-FR.yml
Normal file
177
internal/i18n/locales/fr-FR.yml
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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:
|
||||||
|
gist.header.embed-help:
|
||||||
|
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.github-oauth: Continuer avec un compte GitHub
|
||||||
|
auth.gitea-oauth: Continuer avec un compte Gitea
|
||||||
|
|
||||||
|
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.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 ?
|
||||||
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
|
||||||
182
internal/i18n/locales/hu-HU.yml
Normal file
182
internal/i18n/locales/hu-HU.yml
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
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.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.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-gitea-account: Gitea fiók hozzáadása
|
||||||
|
settings.unlink-github-account: GitHub 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.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.github-oauth: Folytatás GitHub fiókkal
|
||||||
|
auth.gitea-oauth: Folytatás Gitea 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.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.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?
|
||||||
177
internal/i18n/locales/pt-BR.yml
Normal file
177
internal/i18n/locales/pt-BR.yml
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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.share: Compartilhar
|
||||||
|
gist.header.share-help: Copiar link para compartilhar este gist.
|
||||||
|
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.github-oauth: Continuar com conta do GitHub
|
||||||
|
auth.gitea-oauth: Continuar com conta do Gitea
|
||||||
|
|
||||||
|
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.config-link-overridden: sobrescrito
|
||||||
|
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.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?
|
||||||
177
internal/i18n/locales/ru-RU.yml
Normal file
177
internal/i18n/locales/ru-RU.yml
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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.github-oauth: Войти с помощью доступа GitHub
|
||||||
|
auth.gitea-oauth: Войти с помощью доступа Gitea
|
||||||
|
|
||||||
|
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.users.delete_confirm: Вы уверены что хотите удалить этого пользователя?
|
||||||
|
|
||||||
|
admin.gists.title: Название
|
||||||
|
admin.gists.private: Приватный
|
||||||
|
admin.gists.nb-files: Файлов
|
||||||
|
admin.gists.nb-likes: Понравилось
|
||||||
|
admin.gists.delete_confirm: Вы уверены что хотите удалить этот фрагмент?
|
||||||
177
internal/i18n/locales/zh-CN.yml
Normal file
177
internal/i18n/locales/zh-CN.yml
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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.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: Select a 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.github-oauth: 使用 GitHub 账号继续
|
||||||
|
auth.gitea-oauth: 使用 Gitea 账号继续
|
||||||
|
|
||||||
|
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.users.delete_confirm: 你想要删除此用户吗?
|
||||||
|
|
||||||
|
admin.gists.title: 标题
|
||||||
|
admin.gists.private: 私有?
|
||||||
|
admin.gists.nb-files: 文件数
|
||||||
|
admin.gists.nb-likes: 喜欢数
|
||||||
|
admin.gists.delete_confirm: 你想要删除此 Gist 吗?
|
||||||
189
internal/i18n/locales/zh-TW.yml
Normal file
189
internal/i18n/locales/zh-TW.yml
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
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.github-oauth: 用 GitHub 帳號繼續
|
||||||
|
auth.gitlab-oauth: 用 GitLab 帳號繼續
|
||||||
|
auth.gitea-oauth: 用 Gitea 帳號繼續
|
||||||
|
|
||||||
|
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.users.delete_confirm: 您要刪除這個使用者嗎?
|
||||||
|
|
||||||
|
admin.gists.title: 標題
|
||||||
|
admin.gists.private: 是否為私人
|
||||||
|
admin.gists.nb-files: 檔案數
|
||||||
|
admin.gists.nb-likes: 喜歡
|
||||||
|
admin.gists.delete_confirm: 您要刪除這個 Gist 嗎?
|
||||||
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
|
||||||
|
}
|
||||||
113
internal/render/markdown.go
Normal file
113
internal/render/markdown.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
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 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
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ package ssh
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
"github.com/thomiceli/opengist/internal/models"
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"io"
|
"io"
|
||||||
@@ -32,27 +32,36 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
|
|||||||
userName := strings.ToLower(repoFields[0])
|
userName := strings.ToLower(repoFields[0])
|
||||||
gistName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git")
|
gistName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git")
|
||||||
|
|
||||||
gist, err := models.GetGist(userName, gistName)
|
gist, err := db.GetGist(userName, gistName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("gist not found")
|
return errors.New("gist not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
requireLogin, err := models.GetSetting(models.SettingRequireLogin)
|
requireLogin, err := db.GetSetting(db.SettingRequireLogin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("internal server error")
|
return errors.New("internal server error")
|
||||||
}
|
}
|
||||||
|
|
||||||
if verb == "receive-pack" || requireLogin == "1" {
|
// Check for the key if :
|
||||||
pubKey, err := models.SSHKeyExistsForUser(key, gist.UserID)
|
// - 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 == 2 ||
|
||||||
|
gist.ID == 0 ||
|
||||||
|
requireLogin == "1" {
|
||||||
|
|
||||||
|
pubKey, err := db.SSHKeyExistsForUser(key, gist.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.Warn().Msg("Invalid SSH authentication attempt from " + ip)
|
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)
|
errorSsh("Failed to get user by SSH key id", err)
|
||||||
return errors.New("internal server error")
|
return errors.New("internal server error")
|
||||||
}
|
}
|
||||||
_ = models.SSHKeyLastUsedNow(pubKey.Content)
|
_ = db.SSHKeyLastUsedNow(pubKey.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
|
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
|
||||||
@@ -85,7 +94,8 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
|
|||||||
// updatedAt is updated only if serviceType is receive-pack
|
// updatedAt is updated only if serviceType is receive-pack
|
||||||
if verb == "receive-pack" {
|
if verb == "receive-pack" {
|
||||||
_ = gist.SetLastActiveNow()
|
_ = gist.SetLastActiveNow()
|
||||||
_ = gist.UpdatePreviewAndCount()
|
_ = gist.UpdatePreviewAndCount(false)
|
||||||
|
gist.AddInIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/models"
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"io"
|
"io"
|
||||||
@@ -24,7 +24,7 @@ func Start() {
|
|||||||
sshConfig := &ssh.ServerConfig{
|
sshConfig := &ssh.ServerConfig{
|
||||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
|
strKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
|
||||||
_, err := models.SSHKeyDoesExists(strKey)
|
_, err := db.SSHKeyDoesExists(strKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -64,7 +64,7 @@ func listen(serverConfig *ssh.ServerConfig) {
|
|||||||
go func() {
|
go func() {
|
||||||
sConn, channels, reqs, err := ssh.NewServerConn(nConn, serverConfig)
|
sConn, channels, reqs, err := ssh.NewServerConn(nConn, serverConfig)
|
||||||
if err != nil {
|
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)
|
errorSsh("Failed to handshake", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
func SliceContains(slice []string, item string) bool {
|
func RemoveDuplicates[T string | int](sliceList []T) []T {
|
||||||
for _, s := range slice {
|
allKeys := make(map[T]bool)
|
||||||
if s == item {
|
list := []T{}
|
||||||
return true
|
for _, item := range sliceList {
|
||||||
|
if _, value := allKeys[item]; !value {
|
||||||
|
allKeys[item] = true
|
||||||
|
list = append(list, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return list
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,12 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/labstack/echo/v4"
|
"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/config"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
"github.com/thomiceli/opengist/internal/models"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
syncReposFromFS = false
|
|
||||||
syncReposFromDB = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func adminIndex(ctx echo.Context) error {
|
func adminIndex(ctx echo.Context) error {
|
||||||
@@ -31,26 +23,30 @@ func adminIndex(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
setData(ctx, "gitVersion", gitVersion)
|
setData(ctx, "gitVersion", gitVersion)
|
||||||
|
|
||||||
countUsers, err := models.CountAll(&models.User{})
|
countUsers, err := db.CountAll(&db.User{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot count users", err)
|
return errorRes(500, "Cannot count users", err)
|
||||||
}
|
}
|
||||||
setData(ctx, "countUsers", countUsers)
|
setData(ctx, "countUsers", countUsers)
|
||||||
|
|
||||||
countGists, err := models.CountAll(&models.Gist{})
|
countGists, err := db.CountAll(&db.Gist{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot count gists", err)
|
return errorRes(500, "Cannot count gists", err)
|
||||||
}
|
}
|
||||||
setData(ctx, "countGists", countGists)
|
setData(ctx, "countGists", countGists)
|
||||||
|
|
||||||
countKeys, err := models.CountAll(&models.SSHKey{})
|
countKeys, err := db.CountAll(&db.SSHKey{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot count SSH keys", err)
|
return errorRes(500, "Cannot count SSH keys", err)
|
||||||
}
|
}
|
||||||
setData(ctx, "countKeys", countKeys)
|
setData(ctx, "countKeys", countKeys)
|
||||||
|
|
||||||
setData(ctx, "syncReposFromFS", syncReposFromFS)
|
setData(ctx, "syncReposFromFS", actions.IsRunning(actions.SyncReposFromFS))
|
||||||
setData(ctx, "syncReposFromDB", syncReposFromDB)
|
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")
|
return html(ctx, "admin_index.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +56,9 @@ func adminUsers(ctx echo.Context) error {
|
|||||||
setData(ctx, "adminHeaderPage", "users")
|
setData(ctx, "adminHeaderPage", "users")
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
var data []*models.User
|
var data []*db.User
|
||||||
var err error
|
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)
|
return errorRes(500, "Cannot get users", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +75,9 @@ func adminGists(ctx echo.Context) error {
|
|||||||
setData(ctx, "adminHeaderPage", "gists")
|
setData(ctx, "adminHeaderPage", "gists")
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
var data []*models.Gist
|
var data []*db.Gist
|
||||||
var err error
|
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)
|
return errorRes(500, "Cannot get gists", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +90,7 @@ func adminGists(ctx echo.Context) error {
|
|||||||
|
|
||||||
func adminUserDelete(ctx echo.Context) error {
|
func adminUserDelete(ctx echo.Context) error {
|
||||||
userId, _ := strconv.ParseUint(ctx.Param("user"), 10, 64)
|
userId, _ := strconv.ParseUint(ctx.Param("user"), 10, 64)
|
||||||
user, err := models.GetUserById(uint(userId))
|
user, err := db.GetUserById(uint(userId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot retrieve user", err)
|
return errorRes(500, "Cannot retrieve user", err)
|
||||||
}
|
}
|
||||||
@@ -108,7 +104,7 @@ func adminUserDelete(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func adminGistDelete(ctx echo.Context) error {
|
func adminGistDelete(ctx echo.Context) error {
|
||||||
gist, err := models.GetGistByID(ctx.Param("gist"))
|
gist, err := db.GetGistByID(ctx.Param("gist"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot retrieve gist", err)
|
return errorRes(500, "Cannot retrieve gist", err)
|
||||||
}
|
}
|
||||||
@@ -121,67 +117,45 @@ func adminGistDelete(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Cannot delete this gist", err)
|
return errorRes(500, "Cannot delete this gist", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gist.RemoveFromIndex()
|
||||||
|
|
||||||
addFlash(ctx, "Gist has been deleted", "success")
|
addFlash(ctx, "Gist has been deleted", "success")
|
||||||
return redirect(ctx, "/admin-panel/gists")
|
return redirect(ctx, "/admin-panel/gists")
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminSyncReposFromFS(ctx echo.Context) error {
|
func adminSyncReposFromFS(ctx echo.Context) error {
|
||||||
addFlash(ctx, "Syncing repositories from filesystem...", "success")
|
addFlash(ctx, "Syncing repositories from filesystem...", "success")
|
||||||
go func() {
|
go actions.Run(actions.SyncReposFromFS)
|
||||||
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
|
|
||||||
}()
|
|
||||||
return redirect(ctx, "/admin-panel")
|
return redirect(ctx, "/admin-panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminSyncReposFromDB(ctx echo.Context) error {
|
func adminSyncReposFromDB(ctx echo.Context) error {
|
||||||
addFlash(ctx, "Syncing repositories from database...", "success")
|
addFlash(ctx, "Syncing repositories from database...", "success")
|
||||||
go func() {
|
go actions.Run(actions.SyncReposFromDB)
|
||||||
if syncReposFromDB {
|
return redirect(ctx, "/admin-panel")
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range entries {
|
func adminGcRepos(ctx echo.Context) error {
|
||||||
path := strings.Split(e, string(os.PathSeparator))
|
addFlash(ctx, "Garbage collecting repositories...", "success")
|
||||||
gist, _ := models.GetGist(path[len(path)-2], path[len(path)-1])
|
go actions.Run(actions.GitGcRepos)
|
||||||
|
return redirect(ctx, "/admin-panel")
|
||||||
|
}
|
||||||
|
|
||||||
if gist.ID == 0 {
|
func adminSyncGistPreviews(ctx echo.Context) error {
|
||||||
if err := git.DeleteRepository(path[len(path)-2], path[len(path)-1]); err != nil {
|
addFlash(ctx, "Syncing Gist previews...", "success")
|
||||||
log.Error().Err(err).Msg("Cannot delete repository")
|
go actions.Run(actions.SyncGistPreviews)
|
||||||
syncReposFromDB = false
|
return redirect(ctx, "/admin-panel")
|
||||||
return
|
}
|
||||||
}
|
|
||||||
}
|
func adminResetHooks(ctx echo.Context) error {
|
||||||
}
|
addFlash(ctx, "Resetting Git server hooks for all repositories...", "success")
|
||||||
syncReposFromDB = false
|
go actions.Run(actions.ResetHooks)
|
||||||
}()
|
return redirect(ctx, "/admin-panel")
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminIndexGists(ctx echo.Context) error {
|
||||||
|
addFlash(ctx, "Indexing all gists...", "success")
|
||||||
|
go actions.Run(actions.IndexGists)
|
||||||
return redirect(ctx, "/admin-panel")
|
return redirect(ctx, "/admin-panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +163,6 @@ func adminConfig(ctx echo.Context) error {
|
|||||||
setData(ctx, "title", "Configuration")
|
setData(ctx, "title", "Configuration")
|
||||||
setData(ctx, "htmlTitle", "Configuration - Admin panel")
|
setData(ctx, "htmlTitle", "Configuration - Admin panel")
|
||||||
setData(ctx, "adminHeaderPage", "config")
|
setData(ctx, "adminHeaderPage", "config")
|
||||||
setData(ctx, "c", config.C)
|
|
||||||
|
|
||||||
return html(ctx, "admin_config.html")
|
return html(ctx, "admin_config.html")
|
||||||
}
|
}
|
||||||
@@ -198,7 +171,7 @@ func adminSetConfig(ctx echo.Context) error {
|
|||||||
key := ctx.FormValue("key")
|
key := ctx.FormValue("key")
|
||||||
value := ctx.FormValue("value")
|
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)
|
return errorRes(500, "Cannot set setting", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,20 +16,30 @@ import (
|
|||||||
"github.com/markbates/goth/gothic"
|
"github.com/markbates/goth/gothic"
|
||||||
"github.com/markbates/goth/providers/gitea"
|
"github.com/markbates/goth/providers/gitea"
|
||||||
"github.com/markbates/goth/providers/github"
|
"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/rs/zerolog/log"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/models"
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GitHubProvider = "github"
|
||||||
|
GitLabProvider = "gitlab"
|
||||||
|
GiteaProvider = "gitea"
|
||||||
|
OpenIDConnect = "openid-connect"
|
||||||
|
)
|
||||||
|
|
||||||
var title = cases.Title(language.English)
|
var title = cases.Title(language.English)
|
||||||
|
|
||||||
func register(ctx echo.Context) error {
|
func register(ctx echo.Context) error {
|
||||||
setData(ctx, "title", "New account")
|
setData(ctx, "title", tr(ctx, "auth.new-account"))
|
||||||
setData(ctx, "htmlTitle", "New account")
|
setData(ctx, "htmlTitle", "New account")
|
||||||
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
||||||
|
setData(ctx, "isLoginPage", false)
|
||||||
return html(ctx, "auth_form.html")
|
return html(ctx, "auth_form.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +57,7 @@ func processRegister(ctx echo.Context) error {
|
|||||||
|
|
||||||
sess := getSession(ctx)
|
sess := getSession(ctx)
|
||||||
|
|
||||||
dto := new(models.UserDTO)
|
dto := new(db.UserDTO)
|
||||||
if err := ctx.Bind(dto); err != nil {
|
if err := ctx.Bind(dto); err != nil {
|
||||||
return errorRes(400, "Cannot bind data", err)
|
return errorRes(400, "Cannot bind data", err)
|
||||||
}
|
}
|
||||||
@@ -57,7 +67,7 @@ func processRegister(ctx echo.Context) error {
|
|||||||
return html(ctx, "auth_form.html")
|
return html(ctx, "auth_form.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists, err := models.UserExists(dto.Username); err != nil || exists {
|
if exists, err := db.UserExists(dto.Username); err != nil || exists {
|
||||||
addFlash(ctx, "Username already exists", "error")
|
addFlash(ctx, "Username already exists", "error")
|
||||||
return html(ctx, "auth_form.html")
|
return html(ctx, "auth_form.html")
|
||||||
}
|
}
|
||||||
@@ -87,9 +97,10 @@ func processRegister(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func login(ctx echo.Context) error {
|
func login(ctx echo.Context) error {
|
||||||
setData(ctx, "title", "Login")
|
setData(ctx, "title", tr(ctx, "auth.login"))
|
||||||
setData(ctx, "htmlTitle", "Login")
|
setData(ctx, "htmlTitle", "Login")
|
||||||
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
|
||||||
|
setData(ctx, "isLoginPage", true)
|
||||||
return html(ctx, "auth_form.html")
|
return html(ctx, "auth_form.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,15 +112,15 @@ func processLogin(ctx echo.Context) error {
|
|||||||
var err error
|
var err error
|
||||||
sess := getSession(ctx)
|
sess := getSession(ctx)
|
||||||
|
|
||||||
dto := &models.UserDTO{}
|
dto := &db.UserDTO{}
|
||||||
if err = ctx.Bind(dto); err != nil {
|
if err = ctx.Bind(dto); err != nil {
|
||||||
return errorRes(400, "Cannot bind data", err)
|
return errorRes(400, "Cannot bind data", err)
|
||||||
}
|
}
|
||||||
password := dto.Password
|
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) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return errorRes(500, "Cannot get user", err)
|
return errorRes(500, "Cannot get user", err)
|
||||||
}
|
}
|
||||||
@@ -137,20 +148,13 @@ func processLogin(ctx echo.Context) error {
|
|||||||
func oauthCallback(ctx echo.Context) error {
|
func oauthCallback(ctx echo.Context) error {
|
||||||
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
|
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(400, "Cannot complete user auth", err)
|
return errorRes(400, "Cannot complete user auth: "+err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currUser := getUserLogged(ctx)
|
currUser := getUserLogged(ctx)
|
||||||
if currUser != nil {
|
if currUser != nil {
|
||||||
// if user is logged in, link account to user and update its avatar URL
|
// if user is logged in, link account to user and update its avatar URL
|
||||||
switch user.Provider {
|
updateUserProviderInfo(currUser, user.Provider, user)
|
||||||
case "github":
|
|
||||||
currUser.GithubID = user.UserID
|
|
||||||
currUser.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
|
|
||||||
case "gitea":
|
|
||||||
currUser.GiteaID = user.UserID
|
|
||||||
currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = currUser.Update(); err != nil {
|
if err = currUser.Update(); err != nil {
|
||||||
return errorRes(500, "Cannot update user "+title.String(user.Provider)+" id", err)
|
return errorRes(500, "Cannot update user "+title.String(user.Provider)+" id", err)
|
||||||
@@ -161,7 +165,7 @@ func oauthCallback(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if user is not in database, create it
|
// 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 err != nil {
|
||||||
if getData(ctx, "DisableSignup") == true {
|
if getData(ctx, "DisableSignup") == true {
|
||||||
return errorRes(403, "Signing up is disabled", nil)
|
return errorRes(403, "Signing up is disabled", nil)
|
||||||
@@ -171,24 +175,17 @@ func oauthCallback(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Cannot get user", err)
|
return errorRes(500, "Cannot get user", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userDB = &models.User{
|
userDB = &db.User{
|
||||||
Username: user.NickName,
|
Username: user.NickName,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
MD5Hash: fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(user.Email))))),
|
MD5Hash: fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(user.Email))))),
|
||||||
}
|
}
|
||||||
|
|
||||||
// set provider id and avatar URL
|
// set provider id and avatar URL
|
||||||
switch user.Provider {
|
updateUserProviderInfo(userDB, user.Provider, user)
|
||||||
case "github":
|
|
||||||
userDB.GithubID = user.UserID
|
|
||||||
userDB.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
|
|
||||||
case "gitea":
|
|
||||||
userDB.GiteaID = user.UserID
|
|
||||||
userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = userDB.Create(); err != nil {
|
if err = userDB.Create(); err != nil {
|
||||||
if models.IsUniqueConstraintViolation(err) {
|
if db.IsUniqueConstraintViolation(err) {
|
||||||
addFlash(ctx, "Username "+user.NickName+" already exists in Opengist", "error")
|
addFlash(ctx, "Username "+user.NickName+" already exists in Opengist", "error")
|
||||||
return redirect(ctx, "/login")
|
return redirect(ctx, "/login")
|
||||||
}
|
}
|
||||||
@@ -204,10 +201,14 @@ func oauthCallback(ctx echo.Context) error {
|
|||||||
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
switch user.Provider {
|
switch user.Provider {
|
||||||
case "github":
|
case GitHubProvider:
|
||||||
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
|
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"))
|
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 {
|
if err == nil {
|
||||||
@@ -224,7 +225,7 @@ func oauthCallback(ctx echo.Context) error {
|
|||||||
keys = keys[:len(keys)-1]
|
keys = keys[:len(keys)-1]
|
||||||
}
|
}
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
sshKey := models.SSHKey{
|
sshKey := db.SSHKey{
|
||||||
Title: "Added from " + user.Provider,
|
Title: "Added from " + user.Provider,
|
||||||
Content: key,
|
Content: key,
|
||||||
User: *userDB,
|
User: *userDB,
|
||||||
@@ -262,7 +263,7 @@ func oauth(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch provider {
|
switch provider {
|
||||||
case "github":
|
case GitHubProvider:
|
||||||
goth.UseProviders(
|
goth.UseProviders(
|
||||||
github.New(
|
github.New(
|
||||||
config.C.GithubClientKey,
|
config.C.GithubClientKey,
|
||||||
@@ -271,7 +272,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(
|
goth.UseProviders(
|
||||||
gitea.NewCustomisedURL(
|
gitea.NewCustomisedURL(
|
||||||
config.C.GiteaClientKey,
|
config.C.GiteaClientKey,
|
||||||
@@ -282,30 +295,41 @@ func oauth(ctx echo.Context) error {
|
|||||||
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
|
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)
|
currUser := getUserLogged(ctx)
|
||||||
if currUser != nil {
|
if currUser != nil {
|
||||||
isDelete := false
|
// Map each provider to a function that checks the relevant ID in currUser
|
||||||
var err error
|
providerIDCheckMap := map[string]func() bool{
|
||||||
switch provider {
|
GitHubProvider: func() bool { return currUser.GithubID != "" },
|
||||||
case "github":
|
GitLabProvider: func() bool { return currUser.GitlabID != "" },
|
||||||
if currUser.GithubID != "" {
|
GiteaProvider: func() bool { return currUser.GiteaID != "" },
|
||||||
isDelete = true
|
OpenIDConnect: func() bool { return currUser.OIDCID != "" },
|
||||||
err = currUser.DeleteProviderID(provider)
|
|
||||||
}
|
|
||||||
case "gitea":
|
|
||||||
if currUser.GiteaID != "" {
|
|
||||||
isDelete = true
|
|
||||||
err = currUser.DeleteProviderID(provider)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
// Check if the provider is valid and if the user has a linked ID
|
||||||
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
|
// 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, "Account unlinked from "+title.String(provider), "success")
|
||||||
return redirect(ctx, "/settings")
|
return redirect(ctx, "/settings")
|
||||||
}
|
}
|
||||||
@@ -313,7 +337,7 @@ func oauth(ctx echo.Context) error {
|
|||||||
|
|
||||||
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
|
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
|
||||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||||
if provider != "github" && provider != "gitea" {
|
if provider != GitHubProvider && provider != GitLabProvider && provider != GiteaProvider && provider != OpenIDConnect {
|
||||||
return errorRes(400, "Unsupported provider", nil)
|
return errorRes(400, "Unsupported provider", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,11 +360,28 @@ func urlJoin(base string, elem ...string) string {
|
|||||||
return joined
|
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 {
|
func getAvatarUrlFromProvider(provider string, identifier string) string {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "github":
|
case GitHubProvider:
|
||||||
return "https://avatars.githubusercontent.com/u/" + identifier + "?v=4"
|
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))
|
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", identifier))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Cannot get user from Gitea")
|
log.Error().Err(err).Msg("Cannot get user from Gitea")
|
||||||
|
|||||||
@@ -2,31 +2,59 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/google/uuid"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
"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/index"
|
||||||
"gorm.io/gorm"
|
"github.com/thomiceli/opengist/internal/render"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 {
|
func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(ctx echo.Context) error {
|
return func(ctx echo.Context) error {
|
||||||
|
currUser := getUserLogged(ctx)
|
||||||
|
|
||||||
userName := ctx.Param("user")
|
userName := ctx.Param("user")
|
||||||
gistName := ctx.Param("gistname")
|
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 {
|
if err != nil {
|
||||||
return notFound("Gist not found")
|
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)
|
setData(ctx, "gist", gist)
|
||||||
|
|
||||||
if config.C.SshGit {
|
if config.C.SshGit {
|
||||||
@@ -59,12 +87,15 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
|
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setData(ctx, "baseHttpUrl", baseHttpUrl)
|
||||||
|
|
||||||
if config.C.HttpGit {
|
if config.C.HttpGit {
|
||||||
setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
|
setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(ctx, "httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
|
setData(ctx, "httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
|
||||||
setData(ctx, "currentUrl", template.URL(ctx.Request().URL.Path))
|
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()
|
nbCommits, err := gist.NbCommits()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,7 +103,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
setData(ctx, "nbCommits", nbCommits)
|
setData(ctx, "nbCommits", nbCommits)
|
||||||
|
|
||||||
if currUser := getUserLogged(ctx); currUser != nil {
|
if currUser != nil {
|
||||||
hasLiked, err := currUser.HasLiked(gist)
|
hasLiked, err := currUser.HasLiked(gist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot get user like status", err)
|
return errorRes(500, "Cannot get user like status", err)
|
||||||
@@ -80,10 +111,38 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
setData(ctx, "hasLiked", hasLiked)
|
setData(ctx, "hasLiked", hasLiked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if gist.Private > 0 {
|
||||||
|
setData(ctx, "NoIndex", true)
|
||||||
|
}
|
||||||
|
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func allGists(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
var urlPage string
|
var urlPage string
|
||||||
@@ -93,22 +152,24 @@ func allGists(ctx echo.Context) error {
|
|||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
sort := "created"
|
sort := "created"
|
||||||
|
sortText := tr(ctx, "gist.list.sort-by-created")
|
||||||
order := "desc"
|
order := "desc"
|
||||||
orderText := "Recently"
|
orderText := tr(ctx, "gist.list.order-by-desc")
|
||||||
|
|
||||||
if ctx.QueryParam("sort") == "updated" {
|
if ctx.QueryParam("sort") == "updated" {
|
||||||
sort = "updated"
|
sort = "updated"
|
||||||
|
sortText = tr(ctx, "gist.list.sort-by-updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.QueryParam("order") == "asc" {
|
if ctx.QueryParam("order") == "asc" {
|
||||||
order = "asc"
|
order = "asc"
|
||||||
orderText = "Least recently"
|
orderText = tr(ctx, "gist.list.order-by-asc")
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(ctx, "sort", sort)
|
setData(ctx, "sort", sortText)
|
||||||
setData(ctx, "order", orderText)
|
setData(ctx, "order", orderText)
|
||||||
|
|
||||||
var gists []*models.Gist
|
var gists []*db.Gist
|
||||||
var currentUserId uint
|
var currentUserId uint
|
||||||
if userLogged != nil {
|
if userLogged != nil {
|
||||||
currentUserId = userLogged.ID
|
currentUserId = userLogged.ID
|
||||||
@@ -124,12 +185,12 @@ func allGists(ctx echo.Context) error {
|
|||||||
setData(ctx, "searchQuery", ctx.QueryParam("q"))
|
setData(ctx, "searchQuery", ctx.QueryParam("q"))
|
||||||
setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
|
setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
|
||||||
urlPage = "search"
|
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") {
|
} else if strings.HasSuffix(urlctx, "all") {
|
||||||
setData(ctx, "htmlTitle", "All gists")
|
setData(ctx, "htmlTitle", "All gists")
|
||||||
setData(ctx, "mode", "all")
|
setData(ctx, "mode", "all")
|
||||||
urlPage = "all"
|
urlPage = "all"
|
||||||
gists, err = models.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
|
gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
liked := false
|
liked := false
|
||||||
@@ -145,9 +206,9 @@ func allGists(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Error matching regexp", err)
|
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 err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return notFound("User not found")
|
return notFound("User not found")
|
||||||
@@ -156,19 +217,19 @@ func allGists(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
setData(ctx, "fromUser", fromUser)
|
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)
|
return errorRes(500, "Error counting gists", err)
|
||||||
} else {
|
} else {
|
||||||
setData(ctx, "countFromUser", countFromUser)
|
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)
|
return errorRes(500, "Error counting liked gists", err)
|
||||||
} else {
|
} else {
|
||||||
setData(ctx, "countLiked", countLiked)
|
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)
|
return errorRes(500, "Error counting forked gists", err)
|
||||||
} else {
|
} else {
|
||||||
setData(ctx, "countForked", countForked)
|
setData(ctx, "countForked", countForked)
|
||||||
@@ -178,25 +239,34 @@ func allGists(ctx echo.Context) error {
|
|||||||
urlPage = fromUserStr + "/liked"
|
urlPage = fromUserStr + "/liked"
|
||||||
setData(ctx, "htmlTitle", "All gists liked by "+fromUserStr)
|
setData(ctx, "htmlTitle", "All gists liked by "+fromUserStr)
|
||||||
setData(ctx, "mode", "liked")
|
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 {
|
} else if forked {
|
||||||
urlPage = fromUserStr + "/forked"
|
urlPage = fromUserStr + "/forked"
|
||||||
setData(ctx, "htmlTitle", "All gists forked by "+fromUserStr)
|
setData(ctx, "htmlTitle", "All gists forked by "+fromUserStr)
|
||||||
setData(ctx, "mode", "forked")
|
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 {
|
} else {
|
||||||
urlPage = fromUserStr
|
urlPage = fromUserStr
|
||||||
setData(ctx, "htmlTitle", "All gists from "+fromUserStr)
|
setData(ctx, "htmlTitle", "All gists from "+fromUserStr)
|
||||||
setData(ctx, "mode", "fromUser")
|
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 {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching gists", err)
|
return errorRes(500, "Error fetching gists", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = paginate(ctx, gists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
|
if err = paginate(ctx, renderedGists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
|
||||||
return errorRes(404, "Page not found", nil)
|
return errorRes(404, "Page not found", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,35 +274,184 @@ func allGists(ctx echo.Context) error {
|
|||||||
return html(ctx, "all.html")
|
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", tr(ctx, "pagination.previous"))
|
||||||
|
setData(ctx, "nextLabel", tr(ctx, "pagination.next"))
|
||||||
|
setData(ctx, "urlPage", "search")
|
||||||
|
setData(ctx, "urlParams", template.URL("&q="+ctx.QueryParam("q")))
|
||||||
|
setData(ctx, "htmlTitle", "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 {
|
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")
|
revision := ctx.Param("revision")
|
||||||
|
|
||||||
if revision == "" {
|
if revision == "" {
|
||||||
revision = "HEAD"
|
revision = "HEAD"
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := gist.Files(revision)
|
files, err := gist.Files(revision, true)
|
||||||
if err != nil {
|
if _, ok := err.(*git.RevisionNotFoundError); ok {
|
||||||
|
return notFound("Revision not found")
|
||||||
|
} else if err != nil {
|
||||||
return errorRes(500, "Error fetching files", err)
|
return errorRes(500, "Error fetching files", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(files) == 0 {
|
renderedFiles := render.HighlightFiles(files)
|
||||||
return notFound("Revision not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(ctx, "page", "code")
|
setData(ctx, "page", "code")
|
||||||
setData(ctx, "commit", revision)
|
setData(ctx, "commit", revision)
|
||||||
setData(ctx, "files", files)
|
setData(ctx, "files", renderedFiles)
|
||||||
setData(ctx, "revision", revision)
|
setData(ctx, "revision", revision)
|
||||||
setData(ctx, "htmlTitle", gist.Title)
|
setData(ctx, "htmlTitle", gist.Title)
|
||||||
return html(ctx, "gist.html")
|
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')
|
||||||
|
`
|
||||||
|
js = fmt.Sprintf(js, cssUrl,
|
||||||
|
strings.Replace(htmlbuf.String(), "\n", `\n`, -1))
|
||||||
|
ctx.Response().Header().Set("Content-Type", "application/javascript")
|
||||||
|
return plainText(ctx, 200, js)
|
||||||
|
}
|
||||||
|
|
||||||
func revisions(ctx echo.Context) error {
|
func revisions(ctx echo.Context) error {
|
||||||
gist := getData(ctx, "gist").(*models.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
userName := gist.User.Username
|
userName := gist.User.Username
|
||||||
gistName := gist.Uuid
|
gistName := gist.Identifier()
|
||||||
|
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
@@ -253,7 +472,7 @@ func revisions(ctx echo.Context) error {
|
|||||||
emailsSet[strings.ToLower(commit.AuthorEmail)] = struct{}{}
|
emailsSet[strings.ToLower(commit.AuthorEmail)] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
emailsUsers, err := models.GetUsersFromEmails(emailsSet)
|
emailsUsers, err := db.GetUsersFromEmails(emailsSet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching users emails", err)
|
return errorRes(500, "Error fetching users emails", err)
|
||||||
}
|
}
|
||||||
@@ -282,13 +501,13 @@ func processCreate(ctx echo.Context) error {
|
|||||||
return errorRes(400, "Bad request", err)
|
return errorRes(400, "Bad request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dto := new(models.GistDTO)
|
dto := new(db.GistDTO)
|
||||||
var gist *models.Gist
|
var gist *db.Gist
|
||||||
|
|
||||||
if isCreate {
|
if isCreate {
|
||||||
setData(ctx, "htmlTitle", "Create a new gist")
|
setData(ctx, "htmlTitle", "Create a new gist")
|
||||||
} else {
|
} else {
|
||||||
gist = getData(ctx, "gist").(*models.Gist)
|
gist = getData(ctx, "gist").(*db.Gist)
|
||||||
setData(ctx, "htmlTitle", "Edit "+gist.Title)
|
setData(ctx, "htmlTitle", "Edit "+gist.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +515,7 @@ func processCreate(ctx echo.Context) error {
|
|||||||
return errorRes(400, "Cannot bind data", err)
|
return errorRes(400, "Cannot bind data", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.Files = make([]models.FileDTO, 0)
|
dto.Files = make([]db.FileDTO, 0)
|
||||||
fileCounter := 0
|
fileCounter := 0
|
||||||
for i := 0; i < len(ctx.Request().PostForm["content"]); i++ {
|
for i := 0; i < len(ctx.Request().PostForm["content"]); i++ {
|
||||||
name := ctx.Request().PostForm["name"][i]
|
name := ctx.Request().PostForm["name"][i]
|
||||||
@@ -312,7 +531,7 @@ func processCreate(ctx echo.Context) error {
|
|||||||
return errorRes(400, "Invalid character unescaped", err)
|
return errorRes(400, "Invalid character unescaped", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.Files = append(dto.Files, models.FileDTO{
|
dto.Files = append(dto.Files, db.FileDTO{
|
||||||
Filename: strings.Trim(name, " "),
|
Filename: strings.Trim(name, " "),
|
||||||
Content: escapedValue,
|
Content: escapedValue,
|
||||||
})
|
})
|
||||||
@@ -324,7 +543,7 @@ func processCreate(ctx echo.Context) error {
|
|||||||
if isCreate {
|
if isCreate {
|
||||||
return html(ctx, "create.html")
|
return html(ctx, "create.html")
|
||||||
} else {
|
} else {
|
||||||
files, err := gist.Files("HEAD")
|
files, err := gist.Files("HEAD", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching files", err)
|
return errorRes(500, "Error fetching files", err)
|
||||||
}
|
}
|
||||||
@@ -390,39 +609,37 @@ 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 {
|
func toggleVisibility(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*models.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
|
||||||
gist.Private = !gist.Private
|
gist.Private = (gist.Private + 1) % 3
|
||||||
if err := gist.Update(); err != nil {
|
if err := gist.UpdateNoTimestamps(); err != nil {
|
||||||
return errorRes(500, "Error updating this gist", err)
|
return errorRes(500, "Error updating this gist", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFlash(ctx, "Gist visibility has been changed", "success")
|
addFlash(ctx, "Gist visibility has been changed", "success")
|
||||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid)
|
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteGist(ctx echo.Context) error {
|
func deleteGist(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*models.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
|
||||||
err := gist.DeleteRepository()
|
|
||||||
if err != nil {
|
|
||||||
return errorRes(500, "Error deleting the repository", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gist.Delete(); err != nil {
|
if err := gist.Delete(); err != nil {
|
||||||
return errorRes(500, "Error deleting this gist", err)
|
return errorRes(500, "Error deleting this gist", err)
|
||||||
}
|
}
|
||||||
|
gist.RemoveFromIndex()
|
||||||
|
|
||||||
addFlash(ctx, "Gist has been deleted", "success")
|
addFlash(ctx, "Gist has been deleted", "success")
|
||||||
return redirect(ctx, "/")
|
return redirect(ctx, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func like(ctx echo.Context) error {
|
func like(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*models.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
currentUser := getUserLogged(ctx)
|
currentUser := getUserLogged(ctx)
|
||||||
|
|
||||||
hasLiked, err := currentUser.HasLiked(gist)
|
hasLiked, err := currentUser.HasLiked(gist)
|
||||||
@@ -440,7 +657,7 @@ func like(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Error liking/dislking this gist", err)
|
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 != "" {
|
if r := ctx.QueryParam("redirecturl"); r != "" {
|
||||||
redirectTo = r
|
redirectTo = r
|
||||||
}
|
}
|
||||||
@@ -448,7 +665,7 @@ func like(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fork(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)
|
currentUser := getUserLogged(ctx)
|
||||||
|
|
||||||
alreadyForked, err := gist.GetForkParent(currentUser)
|
alreadyForked, err := gist.GetForkParent(currentUser)
|
||||||
@@ -458,11 +675,11 @@ func fork(ctx echo.Context) error {
|
|||||||
|
|
||||||
if gist.User.ID == currentUser.ID {
|
if gist.User.ID == currentUser.ID {
|
||||||
addFlash(ctx, "Unable to fork own gists", "error")
|
addFlash(ctx, "Unable to fork own gists", "error")
|
||||||
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid)
|
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
if alreadyForked.ID != 0 {
|
if alreadyForked.ID != 0 {
|
||||||
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Uuid)
|
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Identifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
uuidGist, err := uuid.NewRandom()
|
uuidGist, err := uuid.NewRandom()
|
||||||
@@ -470,7 +687,7 @@ func fork(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Error creating an UUID", err)
|
return errorRes(500, "Error creating an UUID", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newGist := &models.Gist{
|
newGist := &db.Gist{
|
||||||
Uuid: strings.Replace(uuidGist.String(), "-", "", -1),
|
Uuid: strings.Replace(uuidGist.String(), "-", "", -1),
|
||||||
Title: gist.Title,
|
Title: gist.Title,
|
||||||
Preview: gist.Preview,
|
Preview: gist.Preview,
|
||||||
@@ -495,13 +712,12 @@ func fork(ctx echo.Context) error {
|
|||||||
|
|
||||||
addFlash(ctx, "Gist has been forked", "success")
|
addFlash(ctx, "Gist has been forked", "success")
|
||||||
|
|
||||||
return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Uuid)
|
return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Identifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
func rawFile(ctx echo.Context) error {
|
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)
|
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error getting file content", err)
|
return errorRes(500, "Error getting file content", err)
|
||||||
}
|
}
|
||||||
@@ -513,10 +729,33 @@ func rawFile(ctx echo.Context) error {
|
|||||||
return plainText(ctx, 200, file.Content)
|
return plainText(ctx, 200, file.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func edit(ctx echo.Context) error {
|
func downloadFile(ctx echo.Context) error {
|
||||||
var 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)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching files from repository", err)
|
return errorRes(500, "Error fetching files from repository", err)
|
||||||
}
|
}
|
||||||
@@ -528,10 +767,10 @@ func edit(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadZip(ctx echo.Context) error {
|
func downloadZip(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*models.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
var revision = ctx.Param("revision")
|
revision := ctx.Param("revision")
|
||||||
|
|
||||||
files, err := gist.Files(revision)
|
files, err := gist.Files(revision, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching files from repository", err)
|
return errorRes(500, "Error fetching files from repository", err)
|
||||||
}
|
}
|
||||||
@@ -563,7 +802,7 @@ func downloadZip(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.Response().Header().Set("Content-Type", "application/zip")
|
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())))
|
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(zipFile.Bytes())))
|
||||||
_, err = ctx.Response().Write(zipFile.Bytes())
|
_, err = ctx.Response().Write(zipFile.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -573,7 +812,7 @@ func downloadZip(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func likes(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)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
@@ -582,7 +821,7 @@ func likes(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Error getting users who liked this gist", err)
|
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 {
|
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Identifier()+"/likes", 1); err != nil {
|
||||||
return errorRes(404, "Page not found", nil)
|
return errorRes(404, "Page not found", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,7 +831,7 @@ func likes(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func forks(ctx echo.Context) error {
|
func forks(ctx echo.Context) error {
|
||||||
var gist = getData(ctx, "gist").(*models.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
currentUser := getUserLogged(ctx)
|
currentUser := getUserLogged(ctx)
|
||||||
@@ -606,7 +845,7 @@ func forks(ctx echo.Context) error {
|
|||||||
return errorRes(500, "Error getting users who liked this gist", err)
|
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 {
|
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Identifier()+"/forks", 2); err != nil {
|
||||||
return errorRes(404, "Page not found", nil)
|
return errorRes(404, "Page not found", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,3 +853,39 @@ func forks(ctx echo.Context) error {
|
|||||||
setData(ctx, "revision", "HEAD")
|
setData(ctx, "revision", "HEAD")
|
||||||
return html(ctx, "forks.html")
|
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, "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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
|
||||||
"github.com/thomiceli/opengist/internal/models"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -17,6 +14,14 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
|
"github.com/thomiceli/opengist/internal/memdb"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var routes = []struct {
|
var routes = []struct {
|
||||||
@@ -45,25 +50,31 @@ func gitHttp(ctx echo.Context) error {
|
|||||||
continue
|
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") ||
|
strings.HasSuffix(ctx.Request().URL.Path, "git-upload-pack") ||
|
||||||
ctx.Request().Method == "GET") &&
|
ctx.Request().Method == "GET" && !isInfoRefs
|
||||||
!getData(ctx, "RequireLogin").(bool)
|
|
||||||
|
|
||||||
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
|
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
|
||||||
|
|
||||||
if _, err := os.Stat(repositoryPath); os.IsNotExist(err) {
|
if _, err := os.Stat(repositoryPath); os.IsNotExist(err) {
|
||||||
if err != nil {
|
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
|
// Shows basic auth if :
|
||||||
if noAuth {
|
// - 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 && !getData(ctx, "RequireLogin").(bool) {
|
||||||
return route.handler(ctx)
|
return route.handler(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +93,70 @@ func gitHttp(ctx echo.Context) error {
|
|||||||
return basicAuth(ctx)
|
return basicAuth(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
|
if !isInit && !isInitReceive {
|
||||||
if err != nil {
|
if gist.ID == 0 {
|
||||||
return errorRes(500, "Cannot verify password", err)
|
return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
|
||||||
|
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 := 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.InitRepositoryViaInit(ctx); 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)
|
return route.handler(ctx)
|
||||||
@@ -123,7 +192,7 @@ func pack(ctx echo.Context, serviceType string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositoryPath := ctx.Get("repositoryPath").(string)
|
repositoryPath := getData(ctx, "repositoryPath").(string)
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath)
|
cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath)
|
||||||
@@ -137,9 +206,19 @@ func pack(ctx echo.Context, serviceType string) error {
|
|||||||
|
|
||||||
// updatedAt is updated only if serviceType is receive-pack
|
// updatedAt is updated only if serviceType is receive-pack
|
||||||
if serviceType == "receive-pack" {
|
if serviceType == "receive-pack" {
|
||||||
gist := getData(ctx, "gist").(*models.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
|
||||||
|
if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil {
|
||||||
|
return err
|
||||||
|
} else if hasNoCommits {
|
||||||
|
if err = gist.Delete(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = gist.SetLastActiveNow()
|
_ = gist.SetLastActiveNow()
|
||||||
_ = gist.UpdatePreviewAndCount()
|
_ = gist.UpdatePreviewAndCount(false)
|
||||||
|
gist.AddInIndex()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -148,7 +227,7 @@ func infoRefs(ctx echo.Context) error {
|
|||||||
noCacheHeaders(ctx)
|
noCacheHeaders(ctx)
|
||||||
var service string
|
var service string
|
||||||
|
|
||||||
gist := getData(ctx, "gist").(*models.Gist)
|
gist := getData(ctx, "gist").(*db.Gist)
|
||||||
|
|
||||||
serviceType := ctx.QueryParam("service")
|
serviceType := ctx.QueryParam("service")
|
||||||
if strings.HasPrefix(serviceType, "git-") {
|
if strings.HasPrefix(serviceType, "git-") {
|
||||||
@@ -232,7 +311,7 @@ func basicAuthDecode(encoded string) (string, string, error) {
|
|||||||
|
|
||||||
func sendFile(ctx echo.Context, contentType string) error {
|
func sendFile(ctx echo.Context, contentType string) error {
|
||||||
gitFile := "/" + strings.Join(strings.Split(ctx.Request().URL.Path, "/")[3:], "/")
|
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)
|
fi, err := os.Stat(gitFile)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return errorRes(404, "File not found", nil)
|
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),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,94 +3,138 @@ package web
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/thomiceli/opengist/internal/index"
|
||||||
|
htmlpkg "html"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
"github.com/markbates/goth/gothic"
|
"github.com/markbates/goth/gothic"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
"github.com/thomiceli/opengist/internal/models"
|
"github.com/thomiceli/opengist/internal/i18n"
|
||||||
"html/template"
|
"github.com/thomiceli/opengist/public"
|
||||||
"io"
|
"github.com/thomiceli/opengist/templates"
|
||||||
"io/fs"
|
"golang.org/x/text/language"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var dev = os.Getenv("OG_DEV") == "1"
|
var (
|
||||||
var store *sessions.CookieStore
|
dev bool
|
||||||
var re = regexp.MustCompile("[^a-z0-9]+")
|
store *sessions.CookieStore
|
||||||
var fm = template.FuncMap{
|
re = regexp.MustCompile("[^a-z0-9]+")
|
||||||
"split": strings.Split,
|
fm = template.FuncMap{
|
||||||
"indexByte": strings.IndexByte,
|
"split": strings.Split,
|
||||||
"toInt": func(i string) int64 {
|
"indexByte": strings.IndexByte,
|
||||||
val, _ := strconv.ParseInt(i, 10, 64)
|
"toInt": func(i string) int {
|
||||||
return val
|
val, _ := strconv.Atoi(i)
|
||||||
},
|
return val
|
||||||
"inc": func(i int64) int64 {
|
},
|
||||||
return i + 1
|
"inc": func(i int) int {
|
||||||
},
|
return i + 1
|
||||||
"splitGit": func(i string) []string {
|
},
|
||||||
return strings.FieldsFunc(i, func(r rune) bool {
|
"splitGit": func(i string) []string {
|
||||||
return r == ',' || r == ' '
|
return strings.FieldsFunc(i, func(r rune) bool {
|
||||||
})
|
return r == ',' || r == ' '
|
||||||
},
|
})
|
||||||
"lines": func(i string) []string {
|
},
|
||||||
return strings.Split(i, "\n")
|
"lines": func(i string) []string {
|
||||||
},
|
return strings.Split(i, "\n")
|
||||||
"isMarkdown": func(i string) bool {
|
},
|
||||||
return strings.ToLower(filepath.Ext(i)) == ".md"
|
"isMarkdown": func(i string) bool {
|
||||||
},
|
return strings.ToLower(filepath.Ext(i)) == ".md"
|
||||||
"isCsv": func(i string) bool {
|
},
|
||||||
return strings.ToLower(filepath.Ext(i)) == ".csv"
|
"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" {
|
"csvFile": func(file *git.File) *git.CsvFile {
|
||||||
return nil
|
if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
|
||||||
}
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
csvFile, err := git.ParseCsv(file)
|
csvFile, err := git.ParseCsv(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return csvFile
|
return csvFile
|
||||||
},
|
},
|
||||||
"httpStatusText": http.StatusText,
|
"httpStatusText": http.StatusText,
|
||||||
"loadedTime": func(startTime time.Time) string {
|
"loadedTime": func(startTime time.Time) string {
|
||||||
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
|
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
|
||||||
},
|
},
|
||||||
"slug": func(s string) string {
|
"slug": func(s string) string {
|
||||||
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
|
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
|
||||||
},
|
},
|
||||||
"avatarUrl": func(user *models.User, noGravatar bool) string {
|
"avatarUrl": func(user *db.User, noGravatar bool) string {
|
||||||
if user.AvatarURL != "" {
|
if user.AvatarURL != "" {
|
||||||
return user.AvatarURL
|
return user.AvatarURL
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MD5Hash != "" && !noGravatar {
|
if user.MD5Hash != "" && !noGravatar {
|
||||||
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
|
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultAvatar()
|
return defaultAvatar()
|
||||||
},
|
},
|
||||||
"asset": func(jsfile string) string {
|
"asset": asset,
|
||||||
if dev {
|
"dev": func() bool {
|
||||||
return "http://localhost:16157/" + jsfile
|
return dev
|
||||||
}
|
},
|
||||||
return "/" + manifestEntries[jsfile].File
|
"defaultAvatar": defaultAvatar,
|
||||||
},
|
"visibilityStr": func(visibility db.Visibility, lowercase bool) string {
|
||||||
"defaultAvatar": defaultAvatar,
|
s := "Public"
|
||||||
}
|
switch visibility {
|
||||||
|
case 1:
|
||||||
|
s = "Unlisted"
|
||||||
|
case 2:
|
||||||
|
s = "Private"
|
||||||
|
}
|
||||||
|
|
||||||
var EmbedFS fs.FS
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type Template struct {
|
type Template struct {
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
@@ -100,17 +144,26 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Con
|
|||||||
return t.templates.ExecuteTemplate(w, name, data)
|
return t.templates.ExecuteTemplate(w, name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start() {
|
type Server struct {
|
||||||
|
echo *echo.Echo
|
||||||
|
dev bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(isDev bool) *Server {
|
||||||
|
dev = isDev
|
||||||
store = sessions.NewCookieStore([]byte("opengist"))
|
store = sessions.NewCookieStore([]byte("opengist"))
|
||||||
gothic.Store = store
|
gothic.Store = store
|
||||||
|
|
||||||
assetsFS := echo.MustSubFS(EmbedFS, "public/assets")
|
|
||||||
|
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.HideBanner = true
|
e.HideBanner = true
|
||||||
e.HidePort = 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(dataInit)
|
||||||
|
e.Use(locale)
|
||||||
e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
|
e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
|
||||||
Getter: middleware.MethodFromForm("_method"),
|
Getter: middleware.MethodFromForm("_method"),
|
||||||
}))
|
}))
|
||||||
@@ -129,7 +182,7 @@ func Start() {
|
|||||||
e.Use(middleware.Secure())
|
e.Use(middleware.Secure())
|
||||||
|
|
||||||
e.Renderer = &Template{
|
e.Renderer = &Template{
|
||||||
templates: template.Must(template.New("t").Funcs(fm).ParseFS(EmbedFS, "templates/*/*.html")),
|
templates: template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html")),
|
||||||
}
|
}
|
||||||
e.HTTPErrorHandler = func(er error, ctx echo.Context) {
|
e.HTTPErrorHandler = func(er error, ctx echo.Context) {
|
||||||
if err, ok := er.(*echo.HTTPError); ok {
|
if err, ok := er.(*echo.HTTPError); ok {
|
||||||
@@ -152,23 +205,27 @@ func Start() {
|
|||||||
|
|
||||||
if !dev {
|
if !dev {
|
||||||
parseManifestEntries()
|
parseManifestEntries()
|
||||||
e.GET("/assets/*", cacheControl(echo.WrapHandler(http.StripPrefix("/assets", http.FileServer(http.FS(assetsFS))))))
|
e.GET("/assets/*", cacheControl(echo.WrapHandler(http.FileServer(http.FS(public.Files)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web based routes
|
// Web based routes
|
||||||
g1 := e.Group("")
|
g1 := e.Group("")
|
||||||
{
|
{
|
||||||
g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
if !dev {
|
||||||
TokenLookup: "form:_csrf",
|
g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||||
CookiePath: "/",
|
TokenLookup: "form:_csrf",
|
||||||
CookieHTTPOnly: true,
|
CookiePath: "/",
|
||||||
CookieSameSite: http.SameSiteStrictMode,
|
CookieHTTPOnly: true,
|
||||||
}))
|
CookieSameSite: http.SameSiteStrictMode,
|
||||||
g1.Use(csrfInit)
|
}))
|
||||||
|
g1.Use(csrfInit)
|
||||||
|
}
|
||||||
|
|
||||||
g1.GET("/", create, logged)
|
g1.GET("/", create, logged)
|
||||||
g1.POST("/", processCreate, logged)
|
g1.POST("/", processCreate, logged)
|
||||||
|
|
||||||
|
g1.GET("/healthcheck", healthcheck)
|
||||||
|
|
||||||
g1.GET("/register", register)
|
g1.GET("/register", register)
|
||||||
g1.POST("/register", processRegister)
|
g1.POST("/register", processRegister)
|
||||||
g1.GET("/login", login)
|
g1.GET("/login", login)
|
||||||
@@ -182,7 +239,8 @@ func Start() {
|
|||||||
g1.DELETE("/settings/account", accountDeleteProcess, logged)
|
g1.DELETE("/settings/account", accountDeleteProcess, logged)
|
||||||
g1.POST("/settings/ssh-keys", sshKeysProcess, logged)
|
g1.POST("/settings/ssh-keys", sshKeysProcess, logged)
|
||||||
g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, 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 := g1.Group("/admin-panel")
|
||||||
{
|
{
|
||||||
g2.Use(adminPermission)
|
g2.Use(adminPermission)
|
||||||
@@ -193,12 +251,26 @@ func Start() {
|
|||||||
g2.POST("/gists/:gist/delete", adminGistDelete)
|
g2.POST("/gists/:gist/delete", adminGistDelete)
|
||||||
g2.POST("/sync-fs", adminSyncReposFromFS)
|
g2.POST("/sync-fs", adminSyncReposFromFS)
|
||||||
g2.POST("/sync-db", adminSyncReposFromDB)
|
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.GET("/configuration", adminConfig)
|
||||||
g2.PUT("/set-config", adminSetConfig)
|
g2.PUT("/set-config", adminSetConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.C.HttpGit {
|
||||||
|
e.Any("/init/*", gitHttp, gistNewPushSoftInit)
|
||||||
|
}
|
||||||
|
|
||||||
g1.GET("/all", allGists, checkRequireLogin)
|
g1.GET("/all", allGists, checkRequireLogin)
|
||||||
g1.GET("/search", allGists, checkRequireLogin)
|
|
||||||
|
if index.Enabled() {
|
||||||
|
g1.GET("/search", search, checkRequireLogin)
|
||||||
|
} else {
|
||||||
|
g1.GET("/search", allGists, checkRequireLogin)
|
||||||
|
}
|
||||||
|
|
||||||
g1.GET("/:user", allGists, checkRequireLogin)
|
g1.GET("/:user", allGists, checkRequireLogin)
|
||||||
g1.GET("/:user/liked", allGists, checkRequireLogin)
|
g1.GET("/:user/liked", allGists, checkRequireLogin)
|
||||||
g1.GET("/:user/forked", allGists, checkRequireLogin)
|
g1.GET("/:user/forked", allGists, checkRequireLogin)
|
||||||
@@ -213,39 +285,46 @@ func Start() {
|
|||||||
g3.POST("/visibility", toggleVisibility, logged, writePermission)
|
g3.POST("/visibility", toggleVisibility, logged, writePermission)
|
||||||
g3.POST("/delete", deleteGist, logged, writePermission)
|
g3.POST("/delete", deleteGist, logged, writePermission)
|
||||||
g3.GET("/raw/:revision/:file", rawFile)
|
g3.GET("/raw/:revision/:file", rawFile)
|
||||||
|
g3.GET("/download/:revision/:file", downloadFile)
|
||||||
g3.GET("/edit", edit, logged, writePermission)
|
g3.GET("/edit", edit, logged, writePermission)
|
||||||
g3.POST("/edit", processCreate, logged, writePermission)
|
g3.POST("/edit", processCreate, logged, writePermission)
|
||||||
g3.POST("/like", like, logged)
|
g3.POST("/like", like, logged)
|
||||||
g3.GET("/likes", likes)
|
g3.GET("/likes", likes)
|
||||||
g3.POST("/fork", fork, logged)
|
g3.POST("/fork", fork, logged)
|
||||||
g3.GET("/forks", forks)
|
g3.GET("/forks", forks)
|
||||||
|
g3.PUT("/checkbox", checkbox, logged, writePermission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debugStr := ""
|
|
||||||
// Git HTTP routes
|
// Git HTTP routes
|
||||||
if config.C.HttpGit {
|
if config.C.HttpGit {
|
||||||
e.Any("/:user/:gistname/*", gitHttp, gistInit)
|
e.Any("/:user/:gistname/*", gitHttp, gistSoftInit)
|
||||||
debugStr = " (with Git over HTTP)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Any("/*", noRouteFound)
|
e.Any("/*", noRouteFound)
|
||||||
|
|
||||||
|
return &Server{echo: e, dev: dev}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() {
|
||||||
addr := config.C.HttpHost + ":" + config.C.HttpPort
|
addr := config.C.HttpHost + ":" + config.C.HttpPort
|
||||||
|
|
||||||
if config.C.HttpTLSEnabled {
|
log.Info().Msg("Starting HTTP server on http://" + addr)
|
||||||
log.Info().Msg("Starting HTTPS server on https://" + addr + debugStr)
|
if err := s.echo.Start(addr); err != nil && err != http.ErrServerClosed {
|
||||||
if err := e.StartTLS(addr, config.C.HttpCertFile, config.C.HttpKeyFile); err != nil {
|
log.Fatal().Err(err).Msg("Failed to start HTTP server")
|
||||||
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 (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 {
|
func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(ctx echo.Context) error {
|
return func(ctx echo.Context) error {
|
||||||
ctxValue := context.WithValue(ctx.Request().Context(), dataKey, echo.Map{})
|
ctxValue := context.WithValue(ctx.Request().Context(), dataKey, echo.Map{})
|
||||||
@@ -256,8 +335,55 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
return errorRes(500, "Cannot read settings from database", err)
|
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, "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, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
|
||||||
|
setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
|
||||||
|
|
||||||
|
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)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
@@ -268,9 +394,9 @@ func sessionInit(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
sess := getSession(ctx)
|
sess := getSession(ctx)
|
||||||
if sess.Values["user"] != nil {
|
if sess.Values["user"] != nil {
|
||||||
var err error
|
var err error
|
||||||
var user *models.User
|
var user *db.User
|
||||||
|
|
||||||
if user, err = models.GetUserById(sess.Values["user"].(uint)); err != nil {
|
if user, err = db.GetUserById(sess.Values["user"].(uint)); err != nil {
|
||||||
sess.Values["user"] = nil
|
sess.Values["user"] = nil
|
||||||
saveSession(sess, ctx)
|
saveSession(sess, ctx)
|
||||||
setData(ctx, "userLogged", nil)
|
setData(ctx, "userLogged", nil)
|
||||||
@@ -298,8 +424,8 @@ func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
return func(ctx echo.Context) error {
|
return func(ctx echo.Context) error {
|
||||||
gist := getData(ctx, "gist")
|
gist := getData(ctx, "gist")
|
||||||
user := getUserLogged(ctx)
|
user := getUserLogged(ctx)
|
||||||
if !gist.(*models.Gist).CanWrite(user) {
|
if !gist.(*db.Gist).CanWrite(user) {
|
||||||
return redirect(ctx, "/"+gist.(*models.Gist).User.Username+"/"+gist.(*models.Gist).Uuid)
|
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier())
|
||||||
}
|
}
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
@@ -321,7 +447,7 @@ func logged(next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
if user != nil {
|
if user != nil {
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
return redirect(ctx, "/login")
|
return redirect(ctx, "/all")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +486,7 @@ type Asset struct {
|
|||||||
var manifestEntries map[string]Asset
|
var manifestEntries map[string]Asset
|
||||||
|
|
||||||
func parseManifestEntries() {
|
func parseManifestEntries() {
|
||||||
file, err := EmbedFS.Open("public/manifest.json")
|
file, err := public.Files.Open("manifest.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to open manifest.json")
|
log.Fatal().Err(err).Msg("Failed to open manifest.json")
|
||||||
}
|
}
|
||||||
@@ -377,5 +503,12 @@ func defaultAvatar() string {
|
|||||||
if dev {
|
if dev {
|
||||||
return "http://localhost:16157/default.png"
|
return "http://localhost:16157/default.png"
|
||||||
}
|
}
|
||||||
return "/" + manifestEntries["default.png"].File
|
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
|
||||||
}
|
}
|
||||||
@@ -3,24 +3,29 @@ package web
|
|||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/models"
|
"os"
|
||||||
"golang.org/x/crypto/ssh"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func userSettings(ctx echo.Context) error {
|
func userSettings(ctx echo.Context) error {
|
||||||
user := getUserLogged(ctx)
|
user := getUserLogged(ctx)
|
||||||
|
|
||||||
keys, err := models.GetSSHKeysByUserID(user.ID)
|
keys, err := db.GetSSHKeysByUserID(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot get SSH keys", err)
|
return errorRes(500, "Cannot get SSH keys", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(ctx, "email", user.Email)
|
setData(ctx, "email", user.Email)
|
||||||
setData(ctx, "sshKeys", keys)
|
setData(ctx, "sshKeys", keys)
|
||||||
|
setData(ctx, "hasPassword", user.Password != "")
|
||||||
setData(ctx, "htmlTitle", "Settings")
|
setData(ctx, "htmlTitle", "Settings")
|
||||||
return html(ctx, "settings.html")
|
return html(ctx, "settings.html")
|
||||||
}
|
}
|
||||||
@@ -61,7 +66,7 @@ func accountDeleteProcess(ctx echo.Context) error {
|
|||||||
func sshKeysProcess(ctx echo.Context) error {
|
func sshKeysProcess(ctx echo.Context) error {
|
||||||
user := getUserLogged(ctx)
|
user := getUserLogged(ctx)
|
||||||
|
|
||||||
var dto = new(models.SSHKeyDTO)
|
dto := new(db.SSHKeyDTO)
|
||||||
if err := ctx.Bind(dto); err != nil {
|
if err := ctx.Bind(dto); err != nil {
|
||||||
return errorRes(400, "Cannot bind data", err)
|
return errorRes(400, "Cannot bind data", err)
|
||||||
}
|
}
|
||||||
@@ -92,12 +97,11 @@ func sshKeysProcess(ctx echo.Context) error {
|
|||||||
func sshKeysDelete(ctx echo.Context) error {
|
func sshKeysDelete(ctx echo.Context) error {
|
||||||
user := getUserLogged(ctx)
|
user := getUserLogged(ctx)
|
||||||
keyId, err := strconv.Atoi(ctx.Param("id"))
|
keyId, err := strconv.Atoi(ctx.Param("id"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return redirect(ctx, "/settings")
|
return redirect(ctx, "/settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := models.GetSSHKeyByID(uint(keyId))
|
key, err := db.GetSSHKeyByID(uint(keyId))
|
||||||
|
|
||||||
if err != nil || key.UserID != user.ID {
|
if err != nil || key.UserID != user.ID {
|
||||||
return redirect(ctx, "/settings")
|
return redirect(ctx, "/settings")
|
||||||
@@ -110,3 +114,67 @@ func sshKeysDelete(ctx echo.Context) error {
|
|||||||
addFlash(ctx, "SSH key deleted", "success")
|
addFlash(ctx, "SSH key deleted", "success")
|
||||||
return redirect(ctx, "/settings")
|
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, "Cannot bind data", err)
|
||||||
|
}
|
||||||
|
dto.Username = user.Username
|
||||||
|
|
||||||
|
if err := ctx.Validate(dto); err != nil {
|
||||||
|
addFlash(ctx, validationMessages(&err), "error")
|
||||||
|
return html(ctx, "settings.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := 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, "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, "Cannot bind data", err)
|
||||||
|
}
|
||||||
|
dto.Password = user.Password
|
||||||
|
|
||||||
|
if err := ctx.Validate(dto); err != nil {
|
||||||
|
addFlash(ctx, validationMessages(&err), "error")
|
||||||
|
return redirect(ctx, "/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists, err := db.UserExists(dto.Username); err != nil || exists {
|
||||||
|
addFlash(ctx, "Username already exists", "error")
|
||||||
|
return redirect(ctx, "/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.Rename(
|
||||||
|
filepath.Join(config.C.OpengistHome, "repos", user.Username),
|
||||||
|
filepath.Join(config.C.OpengistHome, "repos", dto.Username))
|
||||||
|
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, "Username updated", "success")
|
||||||
|
return redirect(ctx, "/settings")
|
||||||
|
}
|
||||||
|
|||||||
91
internal/web/test/auth_test.go
Normal file
91
internal/web/test/auth_test.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
256
internal/web/test/gist_test.go
Normal file
256
internal/web/test/gist_test.go
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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", nil, 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", nil, 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", nil, 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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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())
|
||||||
|
}
|
||||||
170
internal/web/test/server.go
Normal file
170
internal/web/test/server.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"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"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testServer struct {
|
||||||
|
server *web.Server
|
||||||
|
sessionCookie string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestServer() (*testServer, error) {
|
||||||
|
s := &testServer{
|
||||||
|
server: web.NewServer(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 != "" {
|
||||||
|
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 {
|
||||||
|
fieldValue := rValue.Field(i).String()
|
||||||
|
v.Add(tag, fieldValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(t *testing.T) {
|
||||||
|
err := config.InitConfig("")
|
||||||
|
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", "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.C.OpengistHome, "tests"))
|
||||||
|
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")
|
||||||
|
}
|
||||||
@@ -10,10 +10,13 @@ import (
|
|||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/thomiceli/opengist/internal/models"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"github.com/thomiceli/opengist/internal/i18n"
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -34,6 +37,10 @@ func getData(ctx echo.Context, key string) any {
|
|||||||
return data[key]
|
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 {
|
func html(ctx echo.Context, template string) error {
|
||||||
return htmlWithCode(ctx, 200, template)
|
return htmlWithCode(ctx, 200, template)
|
||||||
}
|
}
|
||||||
@@ -44,7 +51,7 @@ func htmlWithCode(ctx echo.Context, code int, template string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func redirect(ctx echo.Context, location string) error {
|
func redirect(ctx echo.Context, location string) error {
|
||||||
return ctx.Redirect(302, location)
|
return ctx.Redirect(302, config.C.ExternalUrl+location)
|
||||||
}
|
}
|
||||||
|
|
||||||
func plainText(ctx echo.Context, code int, message string) error {
|
func plainText(ctx echo.Context, code int, message string) error {
|
||||||
@@ -59,10 +66,10 @@ func errorRes(code int, message string, err error) error {
|
|||||||
return &echo.HTTPError{Code: code, Message: message, Internal: err}
|
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")
|
user := getData(ctx, "userLogged")
|
||||||
if user != nil {
|
if user != nil {
|
||||||
return user.(*models.User)
|
return user.(*db.User)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -109,7 +116,7 @@ func deleteCsrfCookie(ctx echo.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadSettings(ctx echo.Context) error {
|
func loadSettings(ctx echo.Context) error {
|
||||||
settings, err := models.GetSettings()
|
settings, err := db.GetSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -129,6 +136,8 @@ type OpengistValidator struct {
|
|||||||
func NewValidator() *OpengistValidator {
|
func NewValidator() *OpengistValidator {
|
||||||
v := validator.New()
|
v := validator.New()
|
||||||
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
|
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
|
||||||
|
_ = v.RegisterValidation("alphanumdash", validateAlphaNumDash)
|
||||||
|
_ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty)
|
||||||
return &OpengistValidator{v}
|
return &OpengistValidator{v}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +161,9 @@ func validationMessages(err *error) string {
|
|||||||
messages[i] = e.Field() + " should not include a sub directory"
|
messages[i] = e.Field() + " should not include a sub directory"
|
||||||
case "alphanum":
|
case "alphanum":
|
||||||
messages[i] = e.Field() + " should only contain alphanumeric characters"
|
messages[i] = e.Field() + " should only contain alphanumeric characters"
|
||||||
|
case "alphanumdash":
|
||||||
|
case "alphanumdashorempty":
|
||||||
|
messages[i] = e.Field() + " should only contain alphanumeric characters and dashes"
|
||||||
case "min":
|
case "min":
|
||||||
messages[i] = "Not enough " + e.Field()
|
messages[i] = "Not enough " + e.Field()
|
||||||
case "notreserved":
|
case "notreserved":
|
||||||
@@ -166,7 +178,7 @@ func validateReservedKeywords(fl validator.FieldLevel) bool {
|
|||||||
name := fl.Field().String()
|
name := fl.Field().String()
|
||||||
|
|
||||||
restrictedNames := map[string]struct{}{}
|
restrictedNames := map[string]struct{}{}
|
||||||
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search"} {
|
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck"} {
|
||||||
restrictedNames[restrictedName] = struct{}{}
|
restrictedNames[restrictedName] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +187,14 @@ func validateReservedKeywords(fl validator.FieldLevel) bool {
|
|||||||
return !ok
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
func getPage(ctx echo.Context) int {
|
func getPage(ctx echo.Context) int {
|
||||||
page := ctx.QueryParam("page")
|
page := ctx.QueryParam("page")
|
||||||
if page == "" {
|
if page == "" {
|
||||||
@@ -211,11 +231,11 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp
|
|||||||
|
|
||||||
switch labels {
|
switch labels {
|
||||||
case 1:
|
case 1:
|
||||||
setData(ctx, "prevLabel", "Previous")
|
setData(ctx, "prevLabel", tr(ctx, "pagination.previous"))
|
||||||
setData(ctx, "nextLabel", "Next")
|
setData(ctx, "nextLabel", tr(ctx, "pagination.next"))
|
||||||
case 2:
|
case 2:
|
||||||
setData(ctx, "prevLabel", "Newer")
|
setData(ctx, "prevLabel", tr(ctx, "pagination.newer"))
|
||||||
setData(ctx, "nextLabel", "Older")
|
setData(ctx, "nextLabel", tr(ctx, "pagination.older"))
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(ctx, "urlPage", urlPage)
|
setData(ctx, "urlPage", urlPage)
|
||||||
@@ -223,6 +243,51 @@ func paginate[T any](ctx echo.Context, data []*T, pageInt int, perPage int, temp
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tr(ctx echo.Context, key string) template.HTML {
|
||||||
|
l := getData(ctx, "locale").(*i18n.Locale)
|
||||||
|
return l.Tr(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content := strings.TrimSpace(contentBuilder.String())
|
||||||
|
return content, metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMetadataToSearchQuery(input, key, value string) string {
|
||||||
|
content, metadata := parseSearchQueryStr(input)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(resultBuilder.String())
|
||||||
|
}
|
||||||
|
|
||||||
type Argon2ID struct {
|
type Argon2ID struct {
|
||||||
format string
|
format string
|
||||||
version int
|
version int
|
||||||
@@ -258,8 +323,16 @@ func (a Argon2ID) hash(plain string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a Argon2ID) verify(plain, hash string) (bool, error) {
|
func (a Argon2ID) verify(plain, hash string) (bool, error) {
|
||||||
|
if hash == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
hashParts := strings.Split(hash, "$")
|
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)
|
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
21
opengist.go
21
opengist.go
@@ -5,8 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"github.com/thomiceli/opengist/internal/git"
|
"github.com/thomiceli/opengist/internal/git"
|
||||||
"github.com/thomiceli/opengist/internal/models"
|
"github.com/thomiceli/opengist/internal/index"
|
||||||
|
"github.com/thomiceli/opengist/internal/memdb"
|
||||||
"github.com/thomiceli/opengist/internal/ssh"
|
"github.com/thomiceli/opengist/internal/ssh"
|
||||||
"github.com/thomiceli/opengist/internal/web"
|
"github.com/thomiceli/opengist/internal/web"
|
||||||
"os"
|
"os"
|
||||||
@@ -36,7 +38,7 @@ func initialize() {
|
|||||||
if ok, err := config.CheckGitVersion(gitVersion); err != nil {
|
if ok, err := config.CheckGitVersion(gitVersion); err != nil {
|
||||||
log.Fatal().Err(err).Send()
|
log.Fatal().Err(err).Send()
|
||||||
} else if !ok {
|
} else if !ok {
|
||||||
log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.20. " +
|
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)
|
"Current git version: " + gitVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,17 +53,26 @@ func initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
|
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
|
||||||
if err := models.Setup(filepath.Join(homePath, config.C.DBFilename)); err != nil {
|
if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), false); err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to initialize database")
|
log.Fatal().Err(err).Msg("Failed to initialize database")
|
||||||
}
|
}
|
||||||
|
|
||||||
web.EmbedFS = dirFS
|
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 main() {
|
func main() {
|
||||||
initialize()
|
initialize()
|
||||||
|
|
||||||
go web.Start()
|
go web.NewServer(os.Getenv("OG_DEV") == "1").Start()
|
||||||
go ssh.Start()
|
go ssh.Start()
|
||||||
|
|
||||||
select {}
|
select {}
|
||||||
|
|||||||
6158
package-lock.json
generated
6158
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "opengist",
|
"name": "opengist",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node_modules/.bin/vite",
|
"dev": "node_modules/.bin/vite -c public/vite.config.js",
|
||||||
"build": "node_modules/.bin/vite build",
|
"build": "node_modules/.bin/vite -c public/vite.config.js build",
|
||||||
"preview": "node_modules/.bin/vite preview"
|
"preview": "node_modules/.bin/vite -c public/vite.config.js preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/commands": "^6.2.2",
|
"@codemirror/commands": "^6.2.2",
|
||||||
@@ -19,15 +18,15 @@
|
|||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"cssnano": "^5.1.15",
|
"cssnano": "^5.1.15",
|
||||||
"github-markdown-css": "^5.2.0",
|
"dayjs": "^1.11.9",
|
||||||
"highlight.js": "^11.7.0",
|
"github-markdown-css": "^5.5.0",
|
||||||
"markdown-it": "^13.0.1",
|
|
||||||
"moment": "^2.29.3",
|
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"postcss": "^8.4.13",
|
"postcss": "^8.4.13",
|
||||||
|
"postcss-cli": "^11.0.0",
|
||||||
"postcss-cssnext": "^3.1.1",
|
"postcss-cssnext": "^3.1.1",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-loader": "^7.1.0",
|
"postcss-loader": "^7.1.0",
|
||||||
|
"postcss-selector-namespace": "^3.0.1",
|
||||||
"sass": "^1.62.1",
|
"sass": "^1.62.1",
|
||||||
"sugarss": "^4.0.1",
|
"sugarss": "^4.0.1",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
|
|||||||
7
postcss.config.js
vendored
7
postcss.config.js
vendored
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
cssnano: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -8,11 +8,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const setSetting = (key: string, value: string) => {
|
const setSetting = (key: string, value: string) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const baseUrl = window.opengist_base_url || '';
|
||||||
const data = new URLSearchParams();
|
const data = new URLSearchParams();
|
||||||
data.append('key', key);
|
data.append('key', key);
|
||||||
data.append('value', value);
|
data.append('value', value);
|
||||||
data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value));
|
if (document.getElementsByName('_csrf').length !== 0) {
|
||||||
return fetch('/admin-panel/set-config', {
|
data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value));
|
||||||
|
}
|
||||||
|
return fetch(`${baseUrl}/admin-panel/set-config`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
body: data,
|
body: data,
|
||||||
|
|||||||
74
public/catppuccin-latte.css
vendored
Normal file
74
public/catppuccin-latte.css
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
.chroma:not(.markdown) { color: #4c4f69 }
|
||||||
|
/* Error */ .chroma .err { color: #d20f39 }
|
||||||
|
/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
|
||||||
|
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
|
||||||
|
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
|
||||||
|
/* LineHighlight */ .chroma .hl { color: #bcc0cc }
|
||||||
|
/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8c8fa1 }
|
||||||
|
/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8c8fa1 }
|
||||||
|
/* Line */ .chroma .line { display: flex; }
|
||||||
|
/* Keyword */ .chroma .k { color: #8839ef }
|
||||||
|
/* KeywordConstant */ .chroma .kc { color: #fe640b }
|
||||||
|
/* KeywordDeclaration */ .chroma .kd { color: #d20f39 }
|
||||||
|
/* KeywordNamespace */ .chroma .kn { color: #179299 }
|
||||||
|
/* KeywordPseudo */ .chroma .kp { color: #8839ef }
|
||||||
|
/* KeywordReserved */ .chroma .kr { color: #8839ef }
|
||||||
|
/* KeywordType */ .chroma .kt { color: #d20f39 }
|
||||||
|
/* NameAttribute */ .chroma .na { color: #1e66f5 }
|
||||||
|
/* NameBuiltin */ .chroma .nb { color: #04a5e5 }
|
||||||
|
/* NameBuiltinPseudo */ .chroma .bp { color: #04a5e5 }
|
||||||
|
/* NameClass */ .chroma .nc { color: #df8e1d }
|
||||||
|
/* NameConstant */ .chroma .no { color: #df8e1d }
|
||||||
|
/* NameDecorator */ .chroma .nd { color: #1e66f5; font-weight: bold }
|
||||||
|
/* NameEntity */ .chroma .ni { color: #179299 }
|
||||||
|
/* NameException */ .chroma .ne { color: #fe640b }
|
||||||
|
/* NameFunction */ .chroma .nf { color: #1e66f5 }
|
||||||
|
/* NameFunctionMagic */ .chroma .fm { color: #1e66f5 }
|
||||||
|
/* NameLabel */ .chroma .nl { color: #04a5e5 }
|
||||||
|
/* NameNamespace */ .chroma .nn { color: #fe640b }
|
||||||
|
/* NameProperty */ .chroma .py { color: #fe640b }
|
||||||
|
/* NameTag */ .chroma .nt { color: #8839ef }
|
||||||
|
/* NameVariable */ .chroma .nv { color: #dc8a78 }
|
||||||
|
/* NameVariableClass */ .chroma .vc { color: #dc8a78 }
|
||||||
|
/* NameVariableGlobal */ .chroma .vg { color: #dc8a78 }
|
||||||
|
/* NameVariableInstance */ .chroma .vi { color: #dc8a78 }
|
||||||
|
/* NameVariableMagic */ .chroma .vm { color: #dc8a78 }
|
||||||
|
/* LiteralString */ .chroma .s { color: #40a02b }
|
||||||
|
/* LiteralStringAffix */ .chroma .sa { color: #d20f39 }
|
||||||
|
/* LiteralStringBacktick */ .chroma .sb { color: #40a02b }
|
||||||
|
/* LiteralStringChar */ .chroma .sc { color: #40a02b }
|
||||||
|
/* LiteralStringDelimiter */ .chroma .dl { color: #1e66f5 }
|
||||||
|
/* LiteralStringDoc */ .chroma .sd { color: #9ca0b0 }
|
||||||
|
/* LiteralStringDouble */ .chroma .s2 { color: #40a02b }
|
||||||
|
/* LiteralStringEscape */ .chroma .se { color: #1e66f5 }
|
||||||
|
/* LiteralStringHeredoc */ .chroma .sh { color: #9ca0b0 }
|
||||||
|
/* LiteralStringInterpol */ .chroma .si { color: #40a02b }
|
||||||
|
/* LiteralStringOther */ .chroma .sx { color: #40a02b }
|
||||||
|
/* LiteralStringRegex */ .chroma .sr { color: #179299 }
|
||||||
|
/* LiteralStringSingle */ .chroma .s1 { color: #40a02b }
|
||||||
|
/* LiteralStringSymbol */ .chroma .ss { color: #40a02b }
|
||||||
|
/* LiteralNumber */ .chroma .m { color: #fe640b }
|
||||||
|
/* LiteralNumberBin */ .chroma .mb { color: #fe640b }
|
||||||
|
/* LiteralNumberFloat */ .chroma .mf { color: #fe640b }
|
||||||
|
/* LiteralNumberHex */ .chroma .mh { color: #fe640b }
|
||||||
|
/* LiteralNumberInteger */ .chroma .mi { color: #fe640b }
|
||||||
|
/* LiteralNumberIntegerLong */ .chroma .il { color: #fe640b }
|
||||||
|
/* LiteralNumberOct */ .chroma .mo { color: #fe640b }
|
||||||
|
/* Operator */ .chroma .o { color: #04a5e5; font-weight: bold }
|
||||||
|
/* OperatorWord */ .chroma .ow { color: #04a5e5; font-weight: bold }
|
||||||
|
/* Comment */ .chroma .c { color: #9ca0b0; font-style: italic }
|
||||||
|
/* CommentHashbang */ .chroma .ch { color: #9ca0b0; font-style: italic }
|
||||||
|
/* CommentMultiline */ .chroma .cm { color: #9ca0b0; font-style: italic }
|
||||||
|
/* CommentSingle */ .chroma .c1 { color: #9ca0b0; font-style: italic }
|
||||||
|
/* CommentSpecial */ .chroma .cs { color: #9ca0b0; font-style: italic }
|
||||||
|
/* CommentPreproc */ .chroma .cp { color: #9ca0b0; font-style: italic }
|
||||||
|
/* CommentPreprocFile */ .chroma .cpf { color: #9ca0b0; font-weight: bold; font-style: italic }
|
||||||
|
/* GenericDeleted */ .chroma .gd { color: #d20f39; background-color: #ccd0da }
|
||||||
|
/* GenericEmph */ .chroma .ge { font-style: italic }
|
||||||
|
/* GenericError */ .chroma .gr { color: #d20f39 }
|
||||||
|
/* GenericHeading */ .chroma .gh { color: #fe640b; font-weight: bold }
|
||||||
|
/* GenericInserted */ .chroma .gi { color: #40a02b; background-color: #ccd0da }
|
||||||
|
/* GenericStrong */ .chroma .gs { font-weight: bold }
|
||||||
|
/* GenericSubheading */ .chroma .gu { color: #fe640b; font-weight: bold }
|
||||||
|
/* GenericTraceback */ .chroma .gt { color: #d20f39 }
|
||||||
|
/* GenericUnderline */ .chroma .gl { text-decoration: underline }
|
||||||
74
public/catppuccin-macchiato.css
vendored
Normal file
74
public/catppuccin-macchiato.css
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
.chroma:not(.markdown) { color: #cad3f5 }
|
||||||
|
/* Error */ .chroma .err { color: #f38ba8 }
|
||||||
|
/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
|
||||||
|
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
|
||||||
|
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
|
||||||
|
/* LineHighlight */ .chroma .hl { color: #45475a }
|
||||||
|
/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f849c }
|
||||||
|
/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f849c }
|
||||||
|
/* Line */ .chroma .line { display: flex; }
|
||||||
|
/* Keyword */ .chroma .k { color: #cba6f7 }
|
||||||
|
/* KeywordConstant */ .chroma .kc { color: #fab387 }
|
||||||
|
/* KeywordDeclaration */ .chroma .kd { color: #f38ba8 }
|
||||||
|
/* KeywordNamespace */ .chroma .kn { color: #94e2d5 }
|
||||||
|
/* KeywordPseudo */ .chroma .kp { color: #cba6f7 }
|
||||||
|
/* KeywordReserved */ .chroma .kr { color: #cba6f7 }
|
||||||
|
/* KeywordType */ .chroma .kt { color: #f38ba8 }
|
||||||
|
/* NameAttribute */ .chroma .na { color: #89b4fa }
|
||||||
|
/* NameBuiltin */ .chroma .nb { color: #89dceb }
|
||||||
|
/* NameBuiltinPseudo */ .chroma .bp { color: #89dceb }
|
||||||
|
/* NameClass */ .chroma .nc { color: #f9e2af }
|
||||||
|
/* NameConstant */ .chroma .no { color: #f9e2af }
|
||||||
|
/* NameDecorator */ .chroma .nd { color: #89b4fa; font-weight: bold }
|
||||||
|
/* NameEntity */ .chroma .ni { color: #94e2d5 }
|
||||||
|
/* NameException */ .chroma .ne { color: #fab387 }
|
||||||
|
/* NameFunction */ .chroma .nf { color: #89b4fa }
|
||||||
|
/* NameFunctionMagic */ .chroma .fm { color: #89b4fa }
|
||||||
|
/* NameLabel */ .chroma .nl { color: #89dceb }
|
||||||
|
/* NameNamespace */ .chroma .nn { color: #fab387 }
|
||||||
|
/* NameProperty */ .chroma .py { color: #fab387 }
|
||||||
|
/* NameTag */ .chroma .nt { color: #cba6f7 }
|
||||||
|
/* NameVariable */ .chroma .nv { color: #f5e0dc }
|
||||||
|
/* NameVariableClass */ .chroma .vc { color: #f5e0dc }
|
||||||
|
/* NameVariableGlobal */ .chroma .vg { color: #f5e0dc }
|
||||||
|
/* NameVariableInstance */ .chroma .vi { color: #f5e0dc }
|
||||||
|
/* NameVariableMagic */ .chroma .vm { color: #f5e0dc }
|
||||||
|
/* LiteralString */ .chroma .s { color: #a6e3a1 }
|
||||||
|
/* LiteralStringAffix */ .chroma .sa { color: #f38ba8 }
|
||||||
|
/* LiteralStringBacktick */ .chroma .sb { color: #a6e3a1 }
|
||||||
|
/* LiteralStringChar */ .chroma .sc { color: #a6e3a1 }
|
||||||
|
/* LiteralStringDelimiter */ .chroma .dl { color: #89b4fa }
|
||||||
|
/* LiteralStringDoc */ .chroma .sd { color: #6c7086 }
|
||||||
|
/* LiteralStringDouble */ .chroma .s2 { color: #a6e3a1 }
|
||||||
|
/* LiteralStringEscape */ .chroma .se { color: #89b4fa }
|
||||||
|
/* LiteralStringHeredoc */ .chroma .sh { color: #6c7086 }
|
||||||
|
/* LiteralStringInterpol */ .chroma .si { color: #a6e3a1 }
|
||||||
|
/* LiteralStringOther */ .chroma .sx { color: #a6e3a1 }
|
||||||
|
/* LiteralStringRegex */ .chroma .sr { color: #94e2d5 }
|
||||||
|
/* LiteralStringSingle */ .chroma .s1 { color: #a6e3a1 }
|
||||||
|
/* LiteralStringSymbol */ .chroma .ss { color: #a6e3a1 }
|
||||||
|
/* LiteralNumber */ .chroma .m { color: #fab387 }
|
||||||
|
/* LiteralNumberBin */ .chroma .mb { color: #fab387 }
|
||||||
|
/* LiteralNumberFloat */ .chroma .mf { color: #fab387 }
|
||||||
|
/* LiteralNumberHex */ .chroma .mh { color: #fab387 }
|
||||||
|
/* LiteralNumberInteger */ .chroma .mi { color: #fab387 }
|
||||||
|
/* LiteralNumberIntegerLong */ .chroma .il { color: #fab387 }
|
||||||
|
/* LiteralNumberOct */ .chroma .mo { color: #fab387 }
|
||||||
|
/* Operator */ .chroma .o { color: #89dceb; font-weight: bold }
|
||||||
|
/* OperatorWord */ .chroma .ow { color: #89dceb; font-weight: bold }
|
||||||
|
/* Comment */ .chroma .c { color: #6c7086; font-style: italic }
|
||||||
|
/* CommentHashbang */ .chroma .ch { color: #6c7086; font-style: italic }
|
||||||
|
/* CommentMultiline */ .chroma .cm { color: #6c7086; font-style: italic }
|
||||||
|
/* CommentSingle */ .chroma .c1 { color: #6c7086; font-style: italic }
|
||||||
|
/* CommentSpecial */ .chroma .cs { color: #6c7086; font-style: italic }
|
||||||
|
/* CommentPreproc */ .chroma .cp { color: #6c7086; font-style: italic }
|
||||||
|
/* CommentPreprocFile */ .chroma .cpf { color: #6c7086; font-weight: bold; font-style: italic }
|
||||||
|
/* GenericDeleted */ .chroma .gd { color: #f38ba8; background-color: #313244 }
|
||||||
|
/* GenericEmph */ .chroma .ge { font-style: italic }
|
||||||
|
/* GenericError */ .chroma .gr { color: #f38ba8 }
|
||||||
|
/* GenericHeading */ .chroma .gh { color: #fab387; font-weight: bold }
|
||||||
|
/* GenericInserted */ .chroma .gi { color: #a6e3a1; background-color: #313244 }
|
||||||
|
/* GenericStrong */ .chroma .gs { font-weight: bold }
|
||||||
|
/* GenericSubheading */ .chroma .gu { color: #fab387; font-weight: bold }
|
||||||
|
/* GenericTraceback */ .chroma .gt { color: #f38ba8 }
|
||||||
|
/* GenericUnderline */ .chroma .gl { text-decoration: underline }
|
||||||
@@ -165,6 +165,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.getElementById('gist-metadata-btn')!.onclick = (el) => {
|
||||||
|
let metadata = document.getElementById('gist-metadata')!;
|
||||||
|
metadata.classList.toggle('hidden');
|
||||||
|
|
||||||
|
let btn = el.target as HTMLButtonElement;
|
||||||
|
if (btn.innerText.endsWith('▼')) {
|
||||||
|
btn.innerText = btn.innerText.replace('▼', '▲');
|
||||||
|
} else {
|
||||||
|
btn.innerText = btn.innerText.replace('▲', '▼');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
document.onsubmit = () => {
|
document.onsubmit = () => {
|
||||||
window.onbeforeunload = null;
|
window.onbeforeunload = null;
|
||||||
};
|
};
|
||||||
|
|||||||
112
public/embed.scss
vendored
Normal file
112
public/embed.scss
vendored
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
@import "github-markdown-css/github-markdown-light";
|
||||||
|
@import './catppuccin-latte';
|
||||||
|
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
@import "github-markdown-css/github-markdown-dark";
|
||||||
|
@import './catppuccin-macchiato';
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@config "./tailwind-embed.config.js";
|
||||||
|
|
||||||
|
.html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
font-feature-settings: normal;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||||
|
font-variation-settings: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ol, ul {
|
||||||
|
list-style: revert;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
font-family: Menlo, Consolas, Liberation Mono, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code .line-num {
|
||||||
|
width: 4%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code td {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code tbody {
|
||||||
|
line-height: 18.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-code {
|
||||||
|
@apply pl-2;
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-num {
|
||||||
|
@apply cursor-pointer text-slate-600 dark:text-slate-400 hover:text-black dark:hover:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.csv-table {
|
||||||
|
@apply w-full whitespace-pre text-xs text-slate-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.csv-table thead {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.csv-table thead tr {
|
||||||
|
@apply bg-slate-100 dark:bg-slate-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.csv-table tbody tr {
|
||||||
|
@apply bg-gray-500 dark:bg-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.csv-table thead tr th {
|
||||||
|
@apply border py-2 px-1 border-slate-300 dark:border-slate-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.csv-table tbody td {
|
||||||
|
@apply border py-1.5 px-1 border-slate-200 dark:border-slate-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.dl-config {
|
||||||
|
@apply grid grid-cols-3 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.dl-config dt {
|
||||||
|
@apply col-span-1 text-gray-700 dark:text-slate-300 font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.dl-config dd {
|
||||||
|
@apply ml-1 col-span-2 break-words;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
@apply dark:bg-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre {
|
||||||
|
@apply flex relative items-start p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .code-div {
|
||||||
|
@apply p-4 max-w-full overflow-x-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code {
|
||||||
|
@apply overflow-auto whitespace-pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chroma.preview.markdown pre code {
|
||||||
|
@apply p-4;
|
||||||
|
}
|
||||||
1
public/embed.ts
Normal file
1
public/embed.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "./embed.scss"
|
||||||
BIN
public/favicon-32.png
Normal file
BIN
public/favicon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" class="w-6 h-6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 294 B |
8
public/fs_embed.go
Normal file
8
public/fs_embed.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build fs_embed
|
||||||
|
|
||||||
|
package public
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed manifest.json assets/*.js assets/*.css assets/*.svg assets/*.png
|
||||||
|
var Files embed.FS
|
||||||
7
public/fs_os.go
Normal file
7
public/fs_os.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !fs_embed
|
||||||
|
|
||||||
|
package public
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
var Files = os.DirFS(".")
|
||||||
76
public/gist.ts
Normal file
76
public/gist.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
|
||||||
|
el.addEventListener('click', event => {
|
||||||
|
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
|
||||||
|
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
|
||||||
|
|
||||||
|
const nextSibling = (event.target as HTMLElement).nextSibling;
|
||||||
|
if (nextSibling instanceof HTMLElement) {
|
||||||
|
nextSibling.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = el.dataset.filenameSlug;
|
||||||
|
const line = (event.target as HTMLElement).textContent;
|
||||||
|
const url = location.protocol + '//' + location.host + location.pathname;
|
||||||
|
const hash = '#file-' + filename + '-' + line;
|
||||||
|
window.history.pushState(null, null, url + hash);
|
||||||
|
location.hash = hash;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let copybtnhtml = `<button type="button" style="top: 1em !important; right: 1em !important;" class="md-code-copy-btn absolute focus-within:z-auto rounded-md dark:border-gray-600 px-2 py-2 opacity-80 font-medium text-slate-700 bg-gray-100 dark:bg-gray-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-gray-600 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z" /></svg></button>`;
|
||||||
|
|
||||||
|
document.querySelectorAll<HTMLElement>('.markdown-body pre').forEach((el) => {
|
||||||
|
el.innerHTML = copybtnhtml + `<span class="code-div">` + el.innerHTML + `</span>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.md-code-copy-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
let code = this.nextElementSibling.textContent;
|
||||||
|
navigator.clipboard.writeText(code).catch((err) => {
|
||||||
|
console.error('Could not copy text: ', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let checkboxes = document.querySelectorAll('li[data-checkbox-nb] input[type=checkbox]');
|
||||||
|
if (document.getElementById('gist').dataset.own) {
|
||||||
|
document.querySelectorAll<HTMLElement>('li[data-checkbox-nb]').forEach((el) => {
|
||||||
|
let input: HTMLButtonElement = el.querySelector('input[type=checkbox]');
|
||||||
|
input.disabled = false;
|
||||||
|
let checkboxNb = (el as HTMLElement).dataset.checkboxNb;
|
||||||
|
let filename = input.closest<HTMLElement>('div[data-file]').dataset.file;
|
||||||
|
|
||||||
|
input.addEventListener('change', function () {
|
||||||
|
const data = new URLSearchParams();
|
||||||
|
data.append('checkbox', checkboxNb);
|
||||||
|
data.append('file', filename);
|
||||||
|
if (document.getElementsByName('_csrf').length !== 0) {
|
||||||
|
data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value));
|
||||||
|
}
|
||||||
|
checkboxes.forEach((el: HTMLButtonElement) => {
|
||||||
|
el.disabled = true;
|
||||||
|
el.classList.add('text-gray-400')
|
||||||
|
});
|
||||||
|
fetch(window.location.href.split('#')[0] + '/checkbox', {
|
||||||
|
method: 'PUT',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: data,
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
checkboxes.forEach((el: HTMLButtonElement) => {
|
||||||
|
el.disabled = false;
|
||||||
|
el.classList.remove('text-gray-400')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
checkboxes.forEach((el: HTMLButtonElement) => {
|
||||||
|
el.disabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
111
public/main.ts
111
public/main.ts
@@ -1,11 +1,13 @@
|
|||||||
import './style.css';
|
import './style.scss';
|
||||||
import './hljs.scss';
|
import './favicon-32.png';
|
||||||
import './favicon.svg';
|
import './opengist.svg';
|
||||||
import './default.png';
|
import './default.png';
|
||||||
import moment from 'moment';
|
import dayjs from 'dayjs';
|
||||||
import md from 'markdown-it';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import hljs from 'highlight.js';
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.extend(localizedFormat);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const themeMenu = document.getElementById('theme-menu')!;
|
const themeMenu = document.getElementById('theme-menu')!;
|
||||||
@@ -14,6 +16,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
localStorage.theme = 'light';
|
localStorage.theme = 'light';
|
||||||
themeMenu.classList.toggle('hidden');
|
themeMenu.classList.toggle('hidden');
|
||||||
|
// @ts-ignore
|
||||||
checkTheme()
|
checkTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +24,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
localStorage.theme = 'dark';
|
localStorage.theme = 'dark';
|
||||||
themeMenu.classList.toggle('hidden');
|
themeMenu.classList.toggle('hidden');
|
||||||
|
// @ts-ignore
|
||||||
checkTheme()
|
checkTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,24 +32,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
localStorage.removeItem('theme');
|
localStorage.removeItem('theme');
|
||||||
themeMenu.classList.toggle('hidden');
|
themeMenu.classList.toggle('hidden');
|
||||||
|
// @ts-ignore
|
||||||
checkTheme();
|
checkTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('theme-btn')!.onclick = (e) => {
|
document.getElementById('theme-btn')!.onclick = () => {
|
||||||
themeMenu.classList.toggle('hidden');
|
themeMenu.classList.toggle('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('user-btn')?.addEventListener("click" , (e) => {
|
document.getElementById('user-btn')?.addEventListener("click" , () => {
|
||||||
document.getElementById('user-menu').classList.toggle('hidden');
|
document.getElementById('user-menu').classList.toggle('hidden');
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelectorAll('.moment-timestamp').forEach((e: HTMLElement) => {
|
document.querySelectorAll('.moment-timestamp').forEach((e: HTMLElement) => {
|
||||||
e.title = moment.unix(parseInt(e.innerHTML)).format('LLLL');
|
e.title = dayjs.unix(parseInt(e.innerHTML)).format('LLLL');
|
||||||
e.innerHTML = moment.unix(parseInt(e.innerHTML)).fromNow();
|
e.innerHTML = dayjs.unix(parseInt(e.innerHTML)).fromNow();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.moment-timestamp-date').forEach((e: HTMLElement) => {
|
document.querySelectorAll('.moment-timestamp-date').forEach((e: HTMLElement) => {
|
||||||
e.innerHTML = moment.unix(parseInt(e.innerHTML)).format('DD/MM/YYYY HH:mm');
|
e.innerHTML = dayjs.unix(parseInt(e.innerHTML)).format('DD/MM/YYYY HH:mm');
|
||||||
});
|
});
|
||||||
|
|
||||||
const rev = document.querySelector<HTMLElement>('.revision-text');
|
const rev = document.querySelector<HTMLElement>('.revision-text');
|
||||||
@@ -62,53 +67,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.markdown').forEach((e: HTMLElement) => {
|
|
||||||
e.innerHTML = md({
|
|
||||||
html: true,
|
|
||||||
highlight: function (str, lang) {
|
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
|
||||||
try {
|
|
||||||
return '<pre class="hljs"><code>' +
|
|
||||||
hljs.highlight(str, {language: lang, ignoreIllegals: true}).value +
|
|
||||||
'</code></pre>';
|
|
||||||
} catch (__) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '<pre class="hljs"><code>' + md().utils.escapeHtml(str) + '</code></pre>';
|
|
||||||
}
|
|
||||||
}).render(e.textContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll<HTMLElement>('.table-code').forEach((el) => {
|
|
||||||
const ext = el.dataset.filename?.split('.').pop() || '';
|
|
||||||
|
|
||||||
if (hljs.autoDetection(ext) && ext !== 'txt') {
|
|
||||||
el.querySelectorAll<HTMLElement>('td.line-code').forEach((ell) => {
|
|
||||||
ell.classList.add('language-' + ext);
|
|
||||||
hljs.highlightElement(ell);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
el.addEventListener('click', event => {
|
|
||||||
if (event.target && (event.target as HTMLElement).matches('.line-num')) {
|
|
||||||
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
|
|
||||||
|
|
||||||
const nextSibling = (event.target as HTMLElement).nextSibling;
|
|
||||||
if (nextSibling instanceof HTMLElement) {
|
|
||||||
nextSibling.classList.add('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const filename = el.dataset.filenameSlug;
|
|
||||||
const line = (event.target as HTMLElement).textContent;
|
|
||||||
const url = location.protocol + '//' + location.host + location.pathname;
|
|
||||||
const hash = '#file-' + filename + '-' + line;
|
|
||||||
window.history.pushState(null, null, url + hash);
|
|
||||||
location.hash = hash;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const colorhash = () => {
|
const colorhash = () => {
|
||||||
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
|
Array.from(document.querySelectorAll('.table-code .selected')).forEach((el) => el.classList.remove('selected'));
|
||||||
@@ -176,11 +134,46 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById('language-btn')!.onclick = () => {
|
||||||
|
document.getElementById('language-list')!.classList.toggle('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
document.querySelectorAll('.copy-gist-btn').forEach((e: HTMLElement) => {
|
document.querySelectorAll('.copy-gist-btn').forEach((e: HTMLElement) => {
|
||||||
e.onclick = () => {
|
e.onclick = () => {
|
||||||
navigator.clipboard.writeText(e.parentNode!.querySelector<HTMLElement>('.gist-content')!.textContent || '').catch((err) => {
|
navigator.clipboard.writeText(e.parentNode!.parentNode!.querySelector<HTMLElement>('.gist-content')!.textContent || '').catch((err) => {
|
||||||
console.error('Could not copy text: ', err);
|
console.error('Could not copy text: ', err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const gistmenuvisibility = document.getElementById('gist-menu-visibility');
|
||||||
|
if (gistmenuvisibility) {
|
||||||
|
let submitgistbutton = (document.getElementById('submit-gist') as HTMLInputElement);
|
||||||
|
document.getElementById('gist-visibility-menu-button')!.onclick = () => {
|
||||||
|
gistmenuvisibility!.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
const lastVisibility = localStorage.getItem('visibility');
|
||||||
|
Array.from(document.querySelectorAll('.gist-visibility-option')).forEach((el) => {
|
||||||
|
const visibility = (el as HTMLElement).dataset.visibility || '0';
|
||||||
|
(el as HTMLElement).onclick = () => {
|
||||||
|
submitgistbutton.textContent = (el as HTMLElement).dataset.btntext;
|
||||||
|
submitgistbutton!.value = visibility;
|
||||||
|
localStorage.setItem('visibility', visibility);
|
||||||
|
gistmenuvisibility!.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (lastVisibility === visibility) {
|
||||||
|
(el as HTMLElement).click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchinput = document.getElementById('search') as HTMLInputElement;
|
||||||
|
searchinput.addEventListener('focusin', () => {
|
||||||
|
document.getElementById('search-help').classList.remove('hidden');
|
||||||
|
})
|
||||||
|
|
||||||
|
searchinput.addEventListener('focusout', (e) => {
|
||||||
|
document.getElementById('search-help').classList.add('hidden');
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
29
public/opengist.svg
Normal file
29
public/opengist.svg
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g id="document" transform="scale(1.6666666666666667 1.6666666666666667) translate(150.0 150.0)">
|
||||||
|
<path class="st0" d="M131.3,24.3c13.7-71-33.9-139.5-106.4-152.9C-47.7-142-117.6-95.3-131.3-24.3s33.9,139.5,106.4,152.9
|
||||||
|
C47.7,142,117.6,95.3,131.3,24.3z"/>
|
||||||
|
<path class="st0" d="M128.9,0c0,55.7-36.8,103-88,119.8c0.2-1.2,0.3-2.5,0.3-4c0.1-22.3,0.2-36.2,0.2-52.8
|
||||||
|
c0-11.7-0.2-18.1-0.2-18.1c1.8,0,21.1-6,29.9-12.1S89.2,15.1,90.5-1.4c1.3-16.6-6-36.2-12.4-47.8C65.3-72.4,54.7-86.6,45.4-94.5
|
||||||
|
c-9.3-7.8-16.1-6.1-22.1-1.4S8.5-76.9,2.2-71.2c-3,2.8-10.6,12-20.4,3.3C-21-70.3-38-93.6-48.5-90.6c-13.1,3.7-28.1,27.3-35.1,43.8
|
||||||
|
c-9,21-10.8,33.6-6.1,63.5c4.7,29.9,7.5,60,11.8,76.4c1,4,2.3,7.4,4,10.4c-33.2-22.8-55-60.7-55-103.5
|
||||||
|
c0-69.7,57.7-126.3,128.9-126.3S128.9-69.7,128.9,0z"/>
|
||||||
|
<path d="M0-145c-81.8,0-148.1,64.9-148.1,145S-81.8,145,0,145S148.1,80.1,148.1,0S81.8-145,0-145z M40.9,119.8
|
||||||
|
c0.2-1.2,0.3-2.5,0.3-4c0.1-22.3,0.2-36.2,0.2-52.8c0-11.7-0.2-18.1-0.2-18.1c1.8,0,21.1-6,29.9-12.1S89.2,15.1,90.5-1.4
|
||||||
|
c1.3-16.6-6-36.2-12.4-47.8C65.3-72.4,54.7-86.6,45.4-94.5c-9.3-7.8-16.1-6.1-22.1-1.4S8.5-76.9,2.2-71.2c-3,2.8-10.6,12-20.4,3.3
|
||||||
|
C-21-70.3-38-93.6-48.5-90.6c-13.1,3.7-28.1,27.3-35.1,43.8c-9,21-10.8,33.6-6.1,63.5c4.7,29.9,7.5,60,11.8,76.4
|
||||||
|
c1,4,2.3,7.4,4,10.4c-33.2-22.8-55-60.7-55-103.5c0-69.7,57.7-126.3,128.9-126.3S128.9-69.7,128.9,0
|
||||||
|
C128.9,55.7,92.1,103,40.9,119.8z"/>
|
||||||
|
<path class="st0" d="M-102.8-7.2l91.2-9.4l-0.3-7l-91.2,9.4L-102.8-7.2z"/>
|
||||||
|
<path class="st0" d="M12-17.3c0.8-9.6-6.5-18-16.3-18.8s-18.4,6.4-19.2,16S-17-2.1-7.2-1.3S11.2-7.7,12-17.3z"/>
|
||||||
|
<path class="st0" d="M62.9-24.6c0.8-9.6-6.5-18-16.3-18.8c-9.8-0.8-18.4,6.4-19.2,16c-0.8,9.6,6.5,18,16.3,18.8S62.1-15,62.9-24.6z
|
||||||
|
"/>
|
||||||
|
<path class="st0" d="M-11.8-16.8l67.6-7.3l-0.5-6.3l-67.5,7.3L-11.8-16.8z"/>
|
||||||
|
<path class="st0" d="M53.1-23.6l49.5-12.2l-0.6-6.3L52.5-29.9L53.1-23.6z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
12
public/postcss.config.js
vendored
Normal file
12
public/postcss.config.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
'postcss-import': {},
|
||||||
|
'tailwindcss/nesting': {},
|
||||||
|
tailwindcss: {
|
||||||
|
config: "./public/tailwind.config.js",
|
||||||
|
},
|
||||||
|
autoprefixer: {},
|
||||||
|
'postcss-selector-namespace': {namespace() {return (process.env.EMBED) ? '.opengist-embed' : '';}},
|
||||||
|
cssnano: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
30
public/style.css
vendored
30
public/style.css
vendored
@@ -2,6 +2,8 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@config "./tailwind.config.js";
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
ul, ol {
|
ul, ol {
|
||||||
list-style: revert;
|
list-style: revert;
|
||||||
@@ -98,10 +100,6 @@ pre {
|
|||||||
max-height: 337px;
|
max-height: 337px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs {
|
|
||||||
color: #c9d1d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-code.selected {
|
.line-code.selected {
|
||||||
background-color: rgb(255, 247, 190) !important;
|
background-color: rgb(255, 247, 190) !important;
|
||||||
box-shadow: inset 4px 0 0 rgb(255, 213, 65) !important;
|
box-shadow: inset 4px 0 0 rgb(255, 213, 65) !important;
|
||||||
@@ -152,3 +150,27 @@ dl.dl-config dt {
|
|||||||
dl.dl-config dd {
|
dl.dl-config dd {
|
||||||
@apply ml-1 col-span-2 break-words;
|
@apply ml-1 col-span-2 break-words;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
@apply dark:bg-gray-900 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre {
|
||||||
|
@apply flex relative items-start p-0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .code-div {
|
||||||
|
@apply p-4 max-w-full overflow-x-auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code {
|
||||||
|
@apply overflow-auto whitespace-pre !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chroma.preview.markdown pre code {
|
||||||
|
@apply p-4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid {
|
||||||
|
background: #f6f8fa !important;
|
||||||
|
}
|
||||||
|
|||||||
6
public/hljs.scss → public/style.scss
vendored
6
public/hljs.scss → public/style.scss
vendored
@@ -1,9 +1,11 @@
|
|||||||
:root {
|
:root {
|
||||||
@import "github-markdown-css/github-markdown-light";
|
@import "github-markdown-css/github-markdown-light";
|
||||||
@import 'highlight.js/scss/base16/one-light.scss';
|
@import './catppuccin-latte';
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@import "github-markdown-css/github-markdown-dark";
|
@import "github-markdown-css/github-markdown-dark";
|
||||||
@import 'highlight.js/scss/base16/onedark.scss';
|
@import './catppuccin-macchiato';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import "style.css";
|
||||||
48
public/tailwind-embed.config.js
vendored
Normal file
48
public/tailwind-embed.config.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const colors = require('tailwindcss/colors')
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./templates/pages/gist_embed.html",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
colors: {
|
||||||
|
white: colors.white,
|
||||||
|
black: colors.black,
|
||||||
|
gray: {
|
||||||
|
50: "#EEEFF1",
|
||||||
|
100: "#DEDFE3",
|
||||||
|
200: "#BABCC5",
|
||||||
|
300: "#999CA8",
|
||||||
|
400: "#75798A",
|
||||||
|
500: "#585B68",
|
||||||
|
600: "#464853",
|
||||||
|
700: "#363840",
|
||||||
|
800: "#232429",
|
||||||
|
900: "#131316"
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
50: '#d6e1ff',
|
||||||
|
100: '#d1dfff',
|
||||||
|
200: '#b9d2fe',
|
||||||
|
300: '#84b1fb',
|
||||||
|
400: '#74a4f6',
|
||||||
|
500: '#588fee',
|
||||||
|
600: '#3c79e2',
|
||||||
|
700: '#356fc0',
|
||||||
|
800: '#2d6195',
|
||||||
|
900: '#2a5574',
|
||||||
|
950: '#173040',
|
||||||
|
},
|
||||||
|
|
||||||
|
slate: colors.slate
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
borderWidth: {
|
||||||
|
'1': '1px',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
darkMode: 'class',
|
||||||
|
}
|
||||||
16
tailwind.config.js → public/tailwind.config.js
vendored
16
tailwind.config.js → public/tailwind.config.js
vendored
@@ -1,5 +1,6 @@
|
|||||||
const colors = require('tailwindcss/colors')
|
const colors = require('tailwindcss/colors')
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./templates/**/*.html",
|
"./templates/**/*.html",
|
||||||
@@ -23,7 +24,20 @@ module.exports = {
|
|||||||
900: "#131316"
|
900: "#131316"
|
||||||
},
|
},
|
||||||
rose: colors.rose,
|
rose: colors.rose,
|
||||||
primary: colors.sky,
|
primary: {
|
||||||
|
50: '#d6e1ff',
|
||||||
|
100: '#d1dfff',
|
||||||
|
200: '#b9d2fe',
|
||||||
|
300: '#84b1fb',
|
||||||
|
400: '#74a4f6',
|
||||||
|
500: '#588fee',
|
||||||
|
600: '#3c79e2',
|
||||||
|
700: '#356fc0',
|
||||||
|
800: '#2d6195',
|
||||||
|
900: '#2a5574',
|
||||||
|
950: '#173040',
|
||||||
|
},
|
||||||
|
|
||||||
slate: colors.slate
|
slate: colors.slate
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
12
public/tsconfig.json
Normal file
12
public/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"main.ts",
|
||||||
|
"editor.ts",
|
||||||
|
"admin.ts",
|
||||||
|
"gist.ts",
|
||||||
|
"embed.ts",
|
||||||
|
],
|
||||||
|
}
|
||||||
22
public/vite.config.js
vendored
Normal file
22
public/vite.config.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: './public',
|
||||||
|
|
||||||
|
build: {
|
||||||
|
// generate manifest.json in outDir
|
||||||
|
outDir: '',
|
||||||
|
assetsDir: 'assets',
|
||||||
|
manifest: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: [
|
||||||
|
'./public/main.ts',
|
||||||
|
'./public/editor.ts',
|
||||||
|
'./public/admin.ts',
|
||||||
|
'./public/gist.ts',
|
||||||
|
'./public/embed.ts'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
assetsInlineLimit: 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
64
scripts/build-all.sh
Executable file
64
scripts/build-all.sh
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
CHECKSUMS_FILE="build/checksums.txt"
|
||||||
|
BINARY_NAME="opengist"
|
||||||
|
TARGETS="darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 linux/armv6 linux/armv7 linux/386 windows/amd64"
|
||||||
|
VERSION=$(git describe --tags | sed 's/^v//')
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "Error: Could not retrieve version from git tags. Exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for TARGET in $TARGETS; do
|
||||||
|
GOOS=${TARGET%/*}
|
||||||
|
GOARCH=${TARGET#*/}
|
||||||
|
|
||||||
|
case $GOOS-$GOARCH in
|
||||||
|
linux-armv6)
|
||||||
|
GOARCH="arm"
|
||||||
|
GOARM=6
|
||||||
|
;;
|
||||||
|
linux-armv7)
|
||||||
|
GOARCH="arm"
|
||||||
|
GOARM=7
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
unset GOARM
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
OUTPUT_PARENT_DIR="build/$GOOS-$GOARCH${GOARM:+v$GOARM}-$VERSION"
|
||||||
|
OUTPUT_DIR="$OUTPUT_PARENT_DIR/$BINARY_NAME"
|
||||||
|
OUTPUT_FILE="$OUTPUT_DIR/$BINARY_NAME"
|
||||||
|
|
||||||
|
if [ "$GOOS" = "windows" ]; then
|
||||||
|
OUTPUT_FILE="$OUTPUT_FILE.exe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building version $VERSION for $GOOS/$GOARCH${GOARM:+v$GOARM}..."
|
||||||
|
mkdir -p $OUTPUT_DIR
|
||||||
|
env GOOS=$GOOS GOARCH=$GOARCH GOARM=$GOARM CGO_ENABLED=0 go build -tags fs_embed -o $OUTPUT_FILE
|
||||||
|
cp README.md $OUTPUT_DIR
|
||||||
|
cp LICENSE $OUTPUT_DIR
|
||||||
|
cp config.yml $OUTPUT_DIR
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error building for $GOOS/$GOARCH${GOARM:+v$GOARM}. Exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Archive the binary with README and LICENSE
|
||||||
|
echo "Archiving for $GOOS/$GOARCH${GOARM:+v$GOARM}..."
|
||||||
|
if [ "$GOOS" = "windows" ]; then
|
||||||
|
# ZIP for Windows
|
||||||
|
cd $OUTPUT_PARENT_DIR && zip -r "../$BINARY_NAME$VERSION-$GOOS-$GOARCH${GOARM:+v$GOARM}.zip" "$BINARY_NAME/" && cd - > /dev/null
|
||||||
|
sha256sum "build/$BINARY_NAME$VERSION-$GOOS-$GOARCH${GOARM:+v$GOARM}.zip" | awk '{print $1 " " substr($2,7)}' >> $CHECKSUMS_FILE
|
||||||
|
else
|
||||||
|
# tar.gz for other platforms
|
||||||
|
tar -czf "build/$BINARY_NAME$VERSION-$GOOS-$GOARCH${GOARM:+v$GOARM}.tar.gz" -C $OUTPUT_PARENT_DIR "$BINARY_NAME"
|
||||||
|
sha256sum "build/$BINARY_NAME$VERSION-$GOOS-$GOARCH${GOARM:+v$GOARM}.tar.gz" | awk '{print $1 " " substr($2,7)}' >> $CHECKSUMS_FILE
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Build and archiving complete."
|
||||||
0
watch.sh → scripts/watch.sh
Normal file → Executable file
0
watch.sh → scripts/watch.sh
Normal file → Executable file
2
templates/base/admin_footer.html
vendored
2
templates/base/admin_footer.html
vendored
@@ -5,7 +5,7 @@
|
|||||||
{{ define "admin_footer" }}
|
{{ define "admin_footer" }}
|
||||||
{{ if .urlPage }}
|
{{ if .urlPage }}
|
||||||
<div class="flex mt-4 justify-center space-x-2">
|
<div class="flex mt-4 justify-center space-x-2">
|
||||||
{{ template "pagination" . }}
|
{{ template "_pagination" . }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
18
templates/base/admin_header.html
vendored
18
templates/base/admin_header.html
vendored
@@ -2,21 +2,21 @@
|
|||||||
<div class="py-10">
|
<div class="py-10">
|
||||||
<header class="pb-4">
|
<header class="pb-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold leading-tight">Admin panel</h1>
|
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "admin.admin_panel" }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="">
|
<div class="">
|
||||||
<nav class="flex space-x-4" aria-label="Tabs">
|
<nav class="flex space-x-4" aria-label="Tabs">
|
||||||
<a href="/admin-panel" class="{{ if eq .adminHeaderPage "index" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
<a href="{{ $.c.ExternalUrl }}/admin-panel" class="{{ if eq .adminHeaderPage "index" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}">General</a>
|
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}">{{ .locale.Tr "admin.general" }} </a>
|
||||||
<a href="/admin-panel/users" class="{{ if eq .adminHeaderPage "users" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
<a href="{{ $.c.ExternalUrl }}/admin-panel/users" class="{{ if eq .adminHeaderPage "users" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">Users</a>
|
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.users" }}</a>
|
||||||
<a href="/admin-panel/gists" class="{{ if eq .adminHeaderPage "gists" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
<a href="{{ $.c.ExternalUrl }}/admin-panel/gists" class="{{ if eq .adminHeaderPage "gists" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">Gists</a>
|
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.gists" }}</a>
|
||||||
<a href="/admin-panel/configuration" class="{{ if eq .adminHeaderPage "config" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
<a href="{{ $.c.ExternalUrl }}/admin-panel/configuration" class="{{ if eq .adminHeaderPage "config" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
|
||||||
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">Configuration</a>
|
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "admin.configuration" }}</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
34
templates/base/base_footer.html
vendored
34
templates/base/base_footer.html
vendored
@@ -4,18 +4,32 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ define "footer" }}
|
{{ define "footer" }}
|
||||||
<p class="text-slate-600 dark:text-slate-400 py-8 [&>*]:mx-1.5 flex">
|
<div class="inline-flex py-8">
|
||||||
<span>
|
<p class="text-slate-600 dark:text-slate-400 [&>*]:mx-1.5 flex">
|
||||||
<a target="_blank" style="margin-left: 0 !important;" class="text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 inline-flex" href="https://github.com/thomiceli/opengist">
|
<span>
|
||||||
<span class="mr-1">Powered by <span class="font-bold">Opengist</span></span>
|
<a target="_blank" style="margin-left: 0 !important;" class="text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 inline-flex" href="https://github.com/thomiceli/opengist">
|
||||||
</a>
|
<span class="mr-1">{{ .locale.Tr "footer.powered-by" "<span class=\"font-bold dark:text-slate-300\">Opengist</span>" }} </span>
|
||||||
</span> ⋅
|
</a>
|
||||||
<span>Load: <span class="font-bold">{{ loadedTime .loadStartTime }}</span></span>
|
</span>⋅
|
||||||
|
<span>Load: <span class="font-bold dark:text-slate-300">{{ loadedTime .loadStartTime }}</span></span>⋅
|
||||||
|
</p>
|
||||||
|
<div class="ml-1.5 cursor-pointer relative inline-block">
|
||||||
|
<span id="language-btn" class="text-slate-600 font-bold dark:text-slate-300"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mb-1 w-5 h-5 inline-flex">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
|
||||||
|
</svg>
|
||||||
|
{{ .localeName }}
|
||||||
|
</span>
|
||||||
|
|
||||||
</p>
|
<div id="language-list" class="hidden absolute bottom-0 z-10 mb-10 mt-2 origin-bottom-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-800 dark:ring-gray-700" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||||
|
<div class="py-1" role="none">
|
||||||
|
{{ range .allLocales }}
|
||||||
|
<a href="?lang={{ .Code }}" class="dark:text-slate-300 text-slate-700 group flex items-center px-4 py-1.5 text-sm w-max hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1" id="menu-item-0">{{ .Name }}</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
115
templates/base/base_header.html
vendored
115
templates/base/base_header.html
vendored
@@ -3,7 +3,14 @@
|
|||||||
<html lang="en" class="h-full">
|
<html lang="en" class="h-full">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
{{ if .NoIndex }}
|
||||||
|
<meta name="robots" content="noindex, follow">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<base href="{{ $.c.ExternalUrl }}" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
window.opengist_base_url = "{{ $.c.ExternalUrl }}";
|
||||||
const checkTheme = () => {
|
const checkTheme = () => {
|
||||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
document.documentElement.classList.add('dark')
|
document.documentElement.classList.add('dark')
|
||||||
@@ -21,10 +28,17 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<link rel="icon" type="image/svg+xml" href="{{ asset "favicon.svg" }}" />
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset "favicon-32.png" }}">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="{{ asset "main.css" }}" />
|
|
||||||
<script type="module" src="{{ asset "main.ts" }}"></script>
|
{{ if dev }}
|
||||||
|
<script type="module" src="{{ asset "@vite/client" }}"></script>
|
||||||
|
<link rel="stylesheet" href="{{ asset "style.scss" }}" />
|
||||||
|
<script type="module" src="{{ asset "main.ts" }}"></script>
|
||||||
|
{{ else }}
|
||||||
|
<link rel="stylesheet" href="{{ asset "main.css" }}" />
|
||||||
|
<script type="module" src="{{ asset "main.ts" }}"></script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ if .htmlTitle }}
|
{{ if .htmlTitle }}
|
||||||
<title>{{ .htmlTitle }} - Opengist</title>
|
<title>{{ .htmlTitle }} - Opengist</title>
|
||||||
@@ -36,7 +50,7 @@
|
|||||||
<div id="app" class="text-gray-700 dark:text-white min-h-full bg-white dark:bg-gray-900">
|
<div id="app" class="text-gray-700 dark:text-white min-h-full bg-white dark:bg-gray-900">
|
||||||
<div class="min-h-full">
|
<div class="min-h-full">
|
||||||
<nav class="dark:bg-gray-800 bg-gray-50">
|
<nav class="dark:bg-gray-800 bg-gray-50">
|
||||||
<div class="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
<div class="max-w-5xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||||
<div class="relative flex items-center justify-between h-16">
|
<div class="relative flex items-center justify-between h-16">
|
||||||
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||||
<!-- Mobile menu button-->
|
<!-- Mobile menu button-->
|
||||||
@@ -50,32 +64,47 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-shrink-0 items-center hidden sm:flex">
|
||||||
|
<a href="{{ $.c.ExternalUrl }}/">
|
||||||
|
<img src="{{ asset "opengist.svg" }}" class="object-cover h-12 w-12">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
<div class="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||||
<div class="flex-shrink-0 flex items-center">
|
<div class="flex-shrink-0 items-center flex sm:hidden">
|
||||||
<a href="/">
|
<a href="{{ $.c.ExternalUrl }}/">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 flex text-primary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
<img src="{{ asset "opengist.svg" }}" class="object-cover h-12 w-12">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
</a>
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden sm:block sm:ml-6">
|
<div class="hidden sm:block sm:ml-6">
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<a href="/all" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">All</a>
|
<a href="{{ $.c.ExternalUrl }}/all" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">{{ .locale.Tr "header.menu.all" }}</a>
|
||||||
<a href="/" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">New</a>
|
<a href="{{ $.c.ExternalUrl }}/{{ if not .userLogged }}login{{ end }}" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">{{ .locale.Tr "header.menu.new" }}</a>
|
||||||
<div class="flex flex-1 items-center justify-center px-2 lg:ml-6 lg:justify-end">
|
<div class="flex flex-1 items-center justify-center px-2 lg:ml-6 lg:justify-end">
|
||||||
<div class="w-full max-w-lg lg:max-w-xs">
|
<div class="w-full max-w-lg lg:max-w-xs">
|
||||||
<label for="search" class="sr-only">Search</label>
|
<label for="search" class="sr-only">{{ .locale.Tr "header.menu.search" }}</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<form action="/search" method="GET">
|
<form action="{{ $.c.ExternalUrl }}/search" method="GET">
|
||||||
<input id="search" name="q" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="Search" type="search" value="{{ .searchQuery }}">
|
<input id="search" name="q" autocomplete="off" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="{{if indexEnabled}}Code search{{else}}Search{{end}}" type="search" value="{{ .searchQuery }}">
|
||||||
<input type="submit" hidden="hidden">
|
<input type="submit" hidden="hidden">
|
||||||
</form>
|
</form>
|
||||||
|
{{if indexEnabled}}
|
||||||
|
<div id="search-help" class="hidden absolute left-1/2 z-10 mt-5 w-screen max-w-max -translate-x-1/2 px-4">
|
||||||
|
<div class="flex-auto overflow-hidden rounded-md bg-white dark:bg-gray-800 text-sm leading-6 border-1 border-gray-100 dark:border-gray-700 ring-1 ring-gray-900/5">
|
||||||
|
<div class="p-4 text-xs space-y-1">
|
||||||
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">user:thomas</code> {{ .locale.Tr "gist.search.help.user" }}</p>
|
||||||
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">title:mygist</code> {{ .locale.Tr "gist.search.help.title" }}</p>
|
||||||
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">filename:myfile.txt</code> {{ .locale.Tr "gist.search.help.filename" }}</p>
|
||||||
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">extension:yml</code> {{ .locale.Tr "gist.search.help.extension" }}</p>
|
||||||
|
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">language:go</code> {{ .locale.Tr "gist.search.help.language" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,44 +121,44 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden relative sm:inline-block text-left">
|
<div class="hidden relative sm:inline-block text-left">
|
||||||
<div id="user-menu" class="hidden w-32 font-medium absolute right-0 z-10 mt-12 origin-top-right divide-y dark:divide-gray-600 divide-gray-100 rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none">
|
<div id="user-menu" class="hidden w-max font-medium absolute right-0 z-10 mt-12 origin-top-right divide-y dark:divide-gray-600 divide-gray-100 rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none">
|
||||||
<div class="py-1" role="none">
|
<div class="py-1" role="none">
|
||||||
<a href="/{{ .userLogged.Username }}" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
<a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||||
</svg>
|
</svg>
|
||||||
My gists
|
{{ .locale.Tr "header.menu.my-gists" }}
|
||||||
</a>
|
</a>
|
||||||
<a href="/{{ .userLogged.Username }}/liked" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
<a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}/liked" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||||
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
|
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
|
||||||
</svg>
|
</svg>
|
||||||
Liked
|
{{ .locale.Tr "header.menu.liked" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{ if .userLogged.IsAdmin }}
|
{{ if .userLogged.IsAdmin }}
|
||||||
<div class="py-1" role="none">
|
<div class="py-1" role="none">
|
||||||
<a href="/admin-panel" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
<a href="{{ $.c.ExternalUrl }}/admin-panel" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
|
||||||
</svg>
|
</svg>
|
||||||
Admin
|
{{ .locale.Tr "header.menu.admin" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="py-1" role="none">
|
<div class="py-1" role="none">
|
||||||
<a href="/settings" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
<a href="{{ $.c.ExternalUrl }}/settings" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
Settings
|
{{ .locale.Tr "header.menu.settings" }}
|
||||||
</a>
|
</a>
|
||||||
<a href="/logout" class="dark:text-rose-400 text-rose-500 group flex items-center px-3 py-1.5 text-sm w-full hover:text-rose-600 dark:hover:text-rose-500" role="menuitem" tabindex="-1">
|
<a href="{{ $.c.ExternalUrl }}/logout" class="dark:text-rose-400 text-rose-500 group flex items-center px-3 py-1.5 pr-6 text-sm w-full hover:text-rose-600 dark:hover:text-rose-500" role="menuitem" tabindex="-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 dark:text-rose-400 text-rose-500 group-hover:text-rose-600 dark:group-hover:text-rose-500">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-3 h-5 w-5 dark:text-rose-400 text-rose-500 group-hover:text-rose-600 dark:group-hover:text-rose-500">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||||
</svg>
|
</svg>
|
||||||
Logout
|
{{ .locale.Tr "header.menu.logout" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,12 +167,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
{{ if not .DisableSignup }}
|
{{ if not .DisableSignup }}
|
||||||
<a href="/register" class="inline-flex text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">
|
<a href="{{ $.c.ExternalUrl }}/register" class="inline-flex text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">
|
||||||
<p class="text-slate-700 dark:text-slate-300 mr-1">Register</p>
|
<p class="text-slate-700 dark:text-slate-300 mr-1">{{ .locale.Tr "header.menu.register" }}</p>
|
||||||
</a>
|
</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<a href="/login" class="inline-flex text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">
|
<a href="{{ $.c.ExternalUrl }}/login" class="inline-flex text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">
|
||||||
<p class="text-slate-700 dark:text-slate-300 mr-1">Login</p>
|
<p class="text-slate-700 dark:text-slate-300 mr-1">{{ .locale.Tr "header.menu.login" }}</p>
|
||||||
</a>
|
</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
@@ -165,19 +194,19 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
Light
|
{{ .locale.Tr "header.menu.light" }}
|
||||||
</button>
|
</button>
|
||||||
<button id="dark-mode" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
<button id="dark-mode" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
</svg>
|
</svg>
|
||||||
Dark
|
{{ .locale.Tr "header.menu.dark" }}
|
||||||
</button>
|
</button>
|
||||||
<button id="system-mode" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
<button id="system-mode" class="dark:text-slate-300 text-slate-700 group flex items-center px-3 py-1.5 text-sm w-max hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="mr-3 h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-slate-500">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
|
||||||
</svg>
|
</svg>
|
||||||
System
|
{{ .locale.Tr "header.menu.system" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,28 +220,28 @@
|
|||||||
<!-- Mobile menu -->
|
<!-- Mobile menu -->
|
||||||
<div class="sm:hidden hidden" id="mobile-menu">
|
<div class="sm:hidden hidden" id="mobile-menu">
|
||||||
<div class="mx-2">
|
<div class="mx-2">
|
||||||
<label for="searchmobile" class="sr-only">Search</label>
|
<label for="searchmobile" class="sr-only">{{ .locale.Tr "header.menu.search" }}</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<form action="/search" method="GET">
|
<form action="{{ $.c.ExternalUrl }}/search" method="GET">
|
||||||
<input id="searchmobile" name="q" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="Search" type="search" value="{{.searchQuery}}">
|
<input id="searchmobile" name="q" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md pl-10" placeholder="Search" type="search" value="{{.searchQuery}}">
|
||||||
<input type="submit" hidden="hidden">
|
<input type="submit" hidden="hidden">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||||
<a href="/all" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">All</a>
|
<a href="{{ $.c.ExternalUrl }}/all" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.all" }}</a>
|
||||||
<a href="/" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">New</a>
|
<a href="{{ $.c.ExternalUrl }}/{{ if not .userLogged }}login{{ end }}" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.new" }}</a>
|
||||||
{{ if .userLogged }}
|
{{ if .userLogged }}
|
||||||
<a href="/{{ .userLogged.Username }}" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">My gists</a>
|
<a href="{{ $.c.ExternalUrl }}/{{ .userLogged.Username }}" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.my-gists" }}</a>
|
||||||
<a href="/settings" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">Settings</a>
|
<a href="{{ $.c.ExternalUrl }}/settings" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.settings" }}</a>
|
||||||
|
|
||||||
{{ if .userLogged.IsAdmin }}
|
{{ if .userLogged.IsAdmin }}
|
||||||
<a href="/admin-panel" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">Admin</a>
|
<a href="{{ $.c.ExternalUrl }}/admin-panel" class="text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white block px-3 py-2 rounded-md text-base font-medium">{{ .locale.Tr "header.menu.admin" }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
@@ -224,7 +253,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-slate-700 dark:text-slate-300">
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-slate-700 dark:text-slate-300">
|
||||||
<div>
|
<div>
|
||||||
{{range .flashErrors}}
|
{{range .flashErrors}}
|
||||||
<div class="mt-4 rounded-md bg-gray-50 dark:bg-gray-800 border-l-4 border-rose-400 p-4">
|
<div class="mt-4 rounded-md bg-gray-50 dark:bg-gray-800 border-l-4 border-rose-400 p-4">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user