Compare commits

...

58 Commits

Author SHA1 Message Date
Thomas Miceli
9936c6bf1e v1.7.0 2024-04-03 02:06:05 +02:00
Thomas Miceli
a97d9cdbf4 Use filesystem session store (#240) 2024-04-03 01:56:55 +02:00
Thomas Miceli
ef004675a5 Create invitations for closed registrations (#233) 2024-04-03 01:56:55 +02:00
Thomas Miceli
3f5f4e01f1 Add custom static links (#234) 2024-04-03 01:56:55 +02:00
Thomas Miceli
c185cb8933 Fix new line literal in embed (#237) 2024-04-03 01:56:55 +02:00
Thomas Miceli
1c1e3a8919 Reset a user password using CLI (#226) 2024-04-03 01:56:55 +02:00
Thomas Miceli
fc9a75ce8f Markdown preview (#224) 2024-04-03 01:56:55 +02:00
Thomas Miceli
2bf0e9b7ce Show theme change button on responsive devices (#225) 2024-04-03 01:56:55 +02:00
Thomas Miceli
e1303c95d0 Increase login for 1 year (#222) 2024-04-03 01:56:55 +02:00
crapStone
915287dc10 Add ability to specify custom names in the OAuth login buttons (#214) 2024-04-03 01:56:55 +02:00
Thomas Miceli
86590d2990 Translations update from Weblate (#210)
Currently translated at 100.0% (180 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/fr/

* Added translation using Weblate (German)

* Translated using Weblate (German)

Currently translated at 26.1% (47 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/de/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (180 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/zh_Hant/

* Translated using Weblate (German)

Currently translated at 60.0% (108 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/de/

* Translated using Weblate (German)

Currently translated at 98.3% (177 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/de/

* Translated using Weblate (German)

Currently translated at 100.0% (180 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/de/

---------

Co-authored-by: crapStone <github@crapstone.dev>
Co-authored-by: Chiawei Chen <qaz855175b@gmail.com>
Co-authored-by: Marcel Herrguth <github@thehomeofanime.de>
Co-authored-by: DerEingerostete <timo.accounts@nachrichtenlager.de>
2024-04-03 01:56:55 +02:00
Thomas Miceli
3179762fd3 Create docker dev env (#220) 2024-04-03 01:56:55 +02:00
Thomas Miceli
86ad88fb09 Set gist URL and title via push options (#216) 2024-04-03 01:56:55 +02:00
Thomas Miceli
db6d6a5eba Set gist visibility via Git push options (#215) 2024-04-03 01:56:55 +02:00
Thomas Miceli
7a75c5ecfa Move Git hook logic to Opengist (#213) 2024-04-03 01:56:55 +02:00
Thomas Miceli
dfe70dc4cf GitHub security updates 2024-04-03 01:56:55 +02:00
Thomas Miceli
afbecd9a1e Add custom logo configuration (#209) 2024-04-03 01:56:55 +02:00
Thomas Miceli
7f4be43bb4 dev-1.7 2024-04-03 01:56:55 +02:00
WilliamNT
05eccfa8e7 Added missing hungarian translations (#207) 2024-02-19 01:59:05 +01:00
Thomas Miceli
a6c4183aac v1.6.1 2024-01-06 14:36:03 +01:00
Thomas Miceli
7fc8577ce0 Translated using Weblate (French) (#201)
Currently translated at 100.0% (180 of 180 strings)

Translation: Opengist/Opengist
Translate-URL: http://tr.opengist.io/projects/_/opengist/fr/
2024-01-06 14:35:08 +01:00
Thomas Miceli
a1524af7a9 Fix directory renaming on username change (#205)
* src/dest dirs have to be lowercase
* if the src dir doesn't exist, don't rename
2024-01-06 14:35:08 +01:00
Thomas Miceli
10cf7e6e25 Add Healthcheck on Docker (#204) 2024-01-06 14:35:08 +01:00
Thomas Miceli
7ce94eea59 Ignore .yml files for Github Actions 2024-01-05 04:36:05 +01:00
Thomas Miceli
8eb8f4e231 v1.6.0 2024-01-04 18:06:19 +01:00
Thomas Miceli
af19268d6f Add some docs (#198) 2024-01-04 18:06:19 +01:00
Thomas Miceli
4215d7e43b Update dependencies (#197)
Go 1.20 -> 1.21
JS package-lock
Nodejs Docker 18 -> 20
Alpine Docker 3.17 -> 3.19
2024-01-04 18:06:19 +01:00
Thomas Miceli
d85917bfb2 Small fixes (#196) 2024-01-04 18:06:19 +01:00
Chiawei Chen
7c1d6e8bfd chore: update taiwan translation (#195) 2024-01-04 18:06:19 +01:00
Matheus C. França
3a2fd2374a Add pt-BR translation (#193)
new translation
2024-01-04 18:06:19 +01:00
Thomas Miceli
87a6113cc7 Add Gist code search (#194) 2024-01-04 18:06:19 +01:00
Thomas Miceli
4cb7dc2d30 Fix reverse proxy subpath support (#192) 2024-01-04 18:06:19 +01:00
Thomas Miceli
f52310a841 Add 2 new admin actions (#191)
* Synchronize all gists previews
* Reset Git server hooks for all repositories
2024-01-04 18:06:19 +01:00
Thomas Miceli
97707f7cca Change username setting (#190) 2024-01-04 18:06:19 +01:00
Thomas Miceli
5058ca8f27 Optimize multiple file rendering (#189) 2024-01-04 18:06:19 +01:00
Thomas Miceli
b3a856a05e Optimize reading gist files content (#186) 2024-01-04 18:06:19 +01:00
WilliamNT
f557bd45df Updated the hungarian translation file (#185) 2024-01-04 18:06:19 +01:00
Jacob Hands
2f8435892e Add config for default branch name (#171)
Co-authored-by: Thomas Miceli <27960254+thomiceli@users.noreply.github.com>
2024-01-04 18:06:19 +01:00
Jacob Hands
4bba26daf6 Add log output config option (#172)
Co-authored-by: Thomas Miceli <27960254+thomiceli@users.noreply.github.com>
2024-01-04 18:06:19 +01:00
Thomas Miceli
3c97901995 Bug fixes (#184)
* Fix gist content when going back to editing

* Fix not outputting non-truncated large files for editon/zip download

* Allow dashes in usernames

* Delete keys associated to deleted user

* Fix error message when there is no files in gist

* Show if there is not files in gist preview

* Fix log parsing for the 11th empty commit
2024-01-04 18:06:19 +01:00
Thomas Miceli
3828022a1c Add custom urls for gists (#183) 2024-01-04 18:06:19 +01:00
Thomas Miceli
85e2da054b Add clickable Markdown checkboxes (#182) 2024-01-04 18:06:19 +01:00
Thomas Miceli
0753c5cb54 Add embedded gists & JSON gist data/metadata (#179) 2024-01-04 18:06:19 +01:00
Thomas Miceli
845e28dd59 Move code rendering to the backend & frontend improvements (#176)
Added Chroma & Goldmark

Added Mermaidjs

More languages supported

Add default values for gist links input

Added copy code from markdown blocks
2024-01-04 18:06:19 +01:00
Chiawei Chen
eff88711ea Trivial Typo: Change 'Gitlab' to 'GitLab' (#177) 2024-01-04 18:06:19 +01:00
Thomas Miceli
8466e50cc3 Add GitLab OAuth provider (#174) 2024-01-04 18:06:19 +01:00
Thomas Miceli
c9fd58c904 Update JS dependencies versions (#175) 2024-01-04 18:06:19 +01:00
Thomas Miceli
47869a77c9 Add healthcheck endpoint (#170) 2024-01-04 18:06:19 +01:00
John Olheiser
246f12c8cb feat: default visibility (#155)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2024-01-04 18:06:19 +01:00
Chiawei Chen
943212e492 feat: add traditional chinese translation (#166) 2024-01-04 18:06:19 +01:00
Pavel Vácha
7a6fb98223 Add Czech translation (#164) 2024-01-04 18:06:19 +01:00
Thomas Miceli
3444fb9b75 v1.5.3 2023-11-20 18:49:46 +01:00
Thomas Miceli
be46304e23 Display OAuth errors (#159) 2023-11-20 18:41:01 +01:00
Thomas Miceli
5fa55dfbba Tiny UI fixes (#158) 2023-11-20 18:28:13 +01:00
Thomas Miceli
09fb647f03 Fix: bare first branch name, truncated output hanging (#157) 2023-11-20 18:03:59 +01:00
Thomas Miceli
d518a44d32 Create/change account password (#156) 2023-11-20 18:03:28 +01:00
Thomas Miceli
dcacde0959 Fix home user directory detection handling (#145) 2023-10-31 15:23:15 +09:00
Manuel Vergara
064d4d53f6 Add spanish translation (#139) 2023-10-31 15:22:58 +09:00
120 changed files with 7687 additions and 5772 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: thomiceli

View File

@@ -5,6 +5,8 @@ on:
- master
- 'dev-*'
pull_request:
paths-ignore:
- '**.yml'
jobs:
lint:
@@ -13,10 +15,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.20
- name: Set up Go 1.21
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.21"
- name: Lint
uses: golangci/golangci-lint-action@v3
@@ -34,10 +36,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.20
- name: Set up Go 1.21
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.21"
- name: Check
run: make go_mod check_changes
@@ -47,7 +49,7 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
go: ["1.20", "1.21"]
go: ["1.21"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout

View File

@@ -13,10 +13,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go 1.20
- name: Set up Go 1.21
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.21"
- name: Cross compile build
run: make all_crosscompile

View File

@@ -1,5 +1,103 @@
# Changelog
## [1.7.0](https://github.com/thomiceli/opengist/compare/v1.6.1...v1.7.0) - 2024-04-03
See here how to [update](/docs/update.md) Opengist.
Note: all sessions will be invalidated after this update.
### Added
- Custom logo configuration (#209)
- Custom static links (#234)
- Invitations for closed registrations (#233)
- Set gist visibility via Git push options (#215)
- Set gist URL and title via push options (#216)
- Specify custom names in the OAuth login buttons (#214)
- Markdown preview (#224)
- Reset a user password using CLI (#226)
- Translations (#207, #210)
### Changed
- Use filesystem session store (#240)
- Move Git hook logic to Opengist (#213)
- Increase login for 1 year (#222)
### Fixed
- Show theme change button on responsive devices (#225)
- New line literal in embed gists (#237)
### Other
- GitHub security updates
- New docker dev env (#220)
## [1.6.1](https://github.com/thomiceli/opengist/compare/v1.6.0...v1.6.1) - 2024-01-06
See here how to [update](/docs/update.md) Opengist.
### Added
- Healthcheck on Docker container (#204)
- Translations:
- fr-FR (#201)
### Fixed
- Directory renaming on username change (#205)
## [1.6.0](https://github.com/thomiceli/opengist/compare/v1.5.3...v1.6.0) - 2024-01-04
See here how to [update](/docs/update.md) Opengist.
### Added
- Embedded gists (#179)
- Gist code search (#194)
- Custom URLS for gists (#183)
- Gist JSON data/metadata (#179)
- Keep default visibility when creating a gist on the UI (#155)
- Health check endpoint (#170)
- GitLab OAuth2 login (#174)
- Syntax highlighting for more file types (#176)
- Checkable Markdown checkboxes (#182)
- Config:
- Log output (#172)
- Default git branch name (#171)
- Change username setting (#190)
- Admin actions:
- Synchronize all gists previews (#191)
- Reset Git server hooks for all repositories (#191)
- Index all gists (#194)
- Translations:
- cs-CZ (#164)
- zh-TW (#166, #195)
- hu-HU (#185)
- pt-BR (#193)
- Docs (#198)
### Changed
- Updated dependencies (#197):
- Go `1.20` -> `1.21`
- JavaScript packages
- NodeJS Docker image `18` -> `20`
- Alpine Docker image `3.17` -> `3.19`
### Fixed
- Fix reverse proxy subpath support (#192)
- Fix undecoded gist content when going back to editing in the UI (#184)
- Fix outputting non-truncated large files for editon/zip download (#184)
- Allow dashes in usernames (#184)
- Delete SSH keys associated to deleted user (#184)
- Better error message when there is no files in gist (#184)
- Show if there is no files in gist preview (#184)
- Log parsing for the 11th empty commit (#184)
- Optimize reading gist files content (#186)
## [1.5.3](https://github.com/thomiceli/opengist/compare/v1.5.2...v1.5.3) - 2023-11-20
### Added
- es-ES translation (#139)
- Create/change account password (#156)
- Display OAuth error messages when HTTP 400 (#159)
### Fixed
- Git bare repository branch name creation (#157)
- Git file truncated output hanging (#157)
- Home user directory detection handling (#145)
- UI changes (#158)
## [1.5.2](https://github.com/thomiceli/opengist/compare/v1.5.1...v1.5.2) - 2023-10-16
### Added
- zh-CN translation (#130)

View File

@@ -1,16 +1,25 @@
FROM alpine:3.17 AS build
FROM alpine:3.19 AS base
RUN apk update && \
apk add --no-cache \
make \
gcc \
musl-dev \
libstdc++
apk add --no-cache \
make \
shadow \
openssl \
openssh \
curl \
wget \
git \
gnupg \
xz \
gcc \
musl-dev \
libstdc++
COPY --from=golang:1.20-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 CGO_ENABLED=0
COPY --from=node:18-alpine /usr/local/ /usr/local/
COPY --from=node:20-alpine /usr/local/ /usr/local/
ENV NODE_PATH="/usr/local/lib/node_modules"
ENV PATH="/usr/local/bin:${PATH}"
@@ -18,10 +27,21 @@ WORKDIR /opengist
COPY . .
FROM base AS dev
EXPOSE 6157 2222 16157
VOLUME /opengist
CMD ["make", "watch"]
FROM base AS build
RUN make
FROM alpine:3.17 as run
FROM alpine:3.19 as prod
RUN apk update && \
apk add --no-cache \
@@ -49,4 +69,5 @@ COPY --from=build --chown=opengist:opengist /opengist/docker ./docker
EXPOSE 6157 2222
VOLUME /opengist
HEALTHCHECK --interval=60s --timeout=30s --start-period=15s --retries=3 CMD curl -f http://localhost:6157/healthcheck || exit 1
ENTRYPOINT ["./docker/entrypoint.sh"]

View File

@@ -1,4 +1,4 @@
.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
.PHONY: all all_crosscompile install build_frontend build_backend build build_crosscompile build_docker build_dev_docker run_dev_docker watch_frontend watch_backend watch clean clean_docker check_changes go_mod fmt test
# Specify the name of your Go binary output
BINARY_NAME := opengist
@@ -15,7 +15,8 @@ install:
build_frontend:
@echo "Building frontend assets..."
npx vite build
npx vite -c public/vite.config.js build
@EMBED=1 npx postcss 'public/assets/embed-*.css' -c public/postcss.config.js --replace # until we can .nest { @tailwind } in Sass
build_backend:
@echo "Building Opengist binary..."
@@ -30,16 +31,23 @@ build_docker:
@echo "Building Docker image..."
docker build -t $(BINARY_NAME):latest .
build_dev_docker:
@echo "Building Docker image..."
docker build -t $(BINARY_NAME)-dev:latest --target dev .
run_dev_docker:
docker run -v .:/opengist -p 6157:6157 -p 16157:16157 $(BINARY_NAME)-dev:latest
watch_frontend:
@echo "Building frontend assets..."
npx vite dev --port 16157
npx vite -c public/vite.config.js dev --port 16157 --host
watch_backend:
@echo "Building Opengist binary..."
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run . --config config.yml'
watch:
@bash ./scripts/watch.sh
@sh ./scripts/watch.sh
clean:
@echo "Cleaning up build artifacts..."

View File

@@ -19,12 +19,13 @@ It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and c
* Create public, unlisted or private snippets
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Revisions history
* Syntax highlighting ; markdown & CSV support
* Search code in snippets ; browse users snippets, likes and forks
* Embed snippets in other websites
* Revisions history
* Like / Fork snippets
* Search for snippets ; browse users snippets, likes and forks
* Download raw files or as a ZIP archive
* OAuth2 login with GitHub, Gitea, and OpenID Connect
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* Restrict or unrestrict snippets visibility to anonymous users
* Docker support
* [More...](/docs/index.md#features)
@@ -36,7 +37,7 @@ It is similiar to [GitHub Gist](https://gist.github.com/), but open-source and c
Docker [images](https://github.com/thomiceli/opengist/pkgs/container/opengist) are available for each release :
```shell
docker pull ghcr.io/thomiceli/opengist:1
docker pull ghcr.io/thomiceli/opengist:1.7
```
It can be used in a `docker-compose.yml` file :
@@ -50,7 +51,7 @@ version: "3"
services:
opengist:
image: ghcr.io/thomiceli/opengist:1
image: ghcr.io/thomiceli/opengist:1.7
container_name: opengist
restart: unless-stopped
ports:
@@ -77,9 +78,9 @@ Download the archive for your system from the release page [here](https://github
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.5.2/opengist1.5.2-linux-amd64.tar.gz
wget https://github.com/thomiceli/opengist/releases/download/v1.7.0/opengist1.7.0-linux-amd64.tar.gz
tar xzvf opengist1.5.2-linux-amd64.tar.gz
tar xzvf opengist1.7.0-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
@@ -89,7 +90,7 @@ Opengist is now running on port 6157, you can browse http://localhost:6157
### From source
Requirements : [Git](https://git-scm.com/downloads) (2.20+), [Go](https://go.dev/doc/install) (1.20+), [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+), [Make](https://linux.die.net/man/1/make) (optional, but easier)
```shell
git clone https://github.com/thomiceli/opengist
@@ -100,6 +101,9 @@ make
Opengist is now running on port 6157, you can browse http://localhost:6157
---
To create and run a development environment, see [run-development.md](/docs/contributing/run-development.md).
## Documentation

View File

@@ -5,8 +5,10 @@
# Set the log level to one of the following: trace, debug, info, warn, error, fatal, panic. Default: warn
log-level: warn
# Public URL for the Git HTTP/SSH connection.
# If not set, uses the URL from the request
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
log-output: stdout,file
# Public URL to access to Opengist
external-url:
# Directory where Opengist will store its data. Default: ~/.opengist/
@@ -15,6 +17,16 @@ opengist-home:
# Name of the SQLite database file. Default: opengist.db
db-filename: opengist.db
# Enable or disable the code search index (either `true` or `false`). Default: true
index.enabled: true
# Name of the directory where the code search index is stored. Default: opengist.index
index.dirname: opengist.index
# Default branch name used by Opengist when initializing Git repositories.
# If not set, uses the Git default branch name. See https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch
git.default-branch:
# Set the journal mode for SQLite. Default: WAL
# See https://www.sqlite.org/pragma.html#pragma_journal_mode
sqlite.journal-mode: WAL
@@ -55,20 +67,44 @@ ssh.keygen-executable: ssh-keygen
# OAuth2 configuration
# The callback/redirect URL must be http://opengist.domain/oauth/<github|gitea|openid-connect>/callback
# The callback/redirect URL must be http://opengist.url/oauth/<github|gitlab|gitea|openid-connect>/callback
# To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new
github.client-key:
github.secret:
# To create a new OAuth2 application using Gitlab : https://gitlab.com/-/user_settings/applications
gitlab.client-key:
gitlab.secret:
# URL of the Gitlab instance. Default: https://gitlab.com/
gitlab.url: https://gitlab.com/
# The name of the GitLab instance. It is displayed in the OAuth login button. Default: GitLab
gitlab.name: GitLab
# To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications
gitea.client-key:
gitea.secret:
# URL of the Gitea instance. Default: https://gitea.com/
gitea.url: https://gitea.com/
# The name of the Gitea instance. It is displayed in the OAuth login button. Default: Gitea
gitea.name: Gitea
# To create a new OAuth2 application using OpenID Connect:
oidc.client-key:
oidc.secret:
# Discovery endpoint of the OpenID provider. Generally something like http://auth.example.com/.well-known/openid-configuration
oidc.discovery-url:
# Custom assets
# Add your own custom assets, that are files relatives to $opengist-home/custom/
custom.logo:
custom.favicon:
# Static pages in footer (like legal notices, privacy policy, etc.)
# The path can be a URL or a relative path to a file in the $opengist-home/custom/ directory
custom.static-links:
# - name: Gitea
# path: https://gitea.com
# - name: Legal notices
# path: legal.html

View 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"}
```

View File

@@ -1,6 +1,10 @@
# Use Nginx as a reverse proxy
Configure Nginx to proxy requests to Opengist. Here is an example configuration file :
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;
@@ -16,7 +20,27 @@ server {
}
```
Then run :
```shell
service nginx restart
### 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
```

View File

@@ -4,8 +4,8 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub, Gite
## Github
* 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`
* 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>
@@ -13,10 +13,23 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub, Gite
```
## 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.domain/oauth/gitea/callback`
* 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>
@@ -29,7 +42,7 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub, Gite
## OpenID Connect
* Add a new OAuth app in Application settings of your OIDC provider
* Set 'Redirect URI' to `http://opengist.domain/oauth/openid-connect/callback`
* 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>

View File

@@ -0,0 +1,7 @@
# Reset a user password
To reset a user password, run the following command using the Opengist binary:
```bash
./opengist admin reset-password <username> <new-password>
```

View File

@@ -1,25 +1,37 @@
# Configuration Cheat Sheet
| YAML Config Key | Environment Variable | Default value | Description |
|-----------------------|--------------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
| 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`) |
| 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. |
| 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. |
| YAML Config Key | Environment Variable | Default value | Description |
|-----------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. |
| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. |
| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. |
| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. |
| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. |
| index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) |
| index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. |
| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) |
| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. |
| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. |
| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) |
| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) |
| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. |
| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. |
| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. |
| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. |
| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. |
| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. |
| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. |
| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. |
| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. |
| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. |
| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. |
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. |
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. |
| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. |
| custom.static-links | OG_CUSTOM_STATIC_LINK_#_(PATH,NAME) | none | Path and name to custom links, more info [here](custom-links.md). |

View File

@@ -0,0 +1,31 @@
# Custom assets
To add custom assets to your Opengist instance, you can use the `$opengist-home/custom` directory (where `$opengist-home` is the directory where Opengist stores its data).
### Logo / Favicon
To add a custom logo or favicon, you can add your own image file to the `$opengist-home/custom` directory, then define the relative path in the config.
For example, if you have a logo file `logo.png` in the `$opengist-home/custom` directory, you can set the logo path in the config as follows:
#### YAML
```yaml
custom.logo: logo.png
```
#### Environment variable
```sh
export OG_CUSTOM_LOGO=logo.png
```
Same as the favicon:
#### YAML
```yaml
custom.favicon: favicon.png
```
#### Environment variable
```sh
export OG_CUSTOM_FAVICON=favicon.png
```

View File

@@ -0,0 +1,38 @@
# Custom links
On the footer of your Opengist instance, you can add links to custom static templates or any other website you want to link to.
This can be useful for legal information, privacy policy, or any other information you want to provide to your users.
To add one or more links, you can add your own file to the `$opengist-home/custom` directory or set a URL, then define the relative path and its name in the config.
For example, if you have a legal information file `legal.html` in the `$opengist-home/custom` directory, and also wish to add a link to a Gitea instance, you can set the link in the config as follows:
#### YAML
```yaml
custom.static-links:
- name: Legal notices
path: legal.html
- name: Gitea
path: https://gitea.com
```
#### Environment variable
```sh
OG_CUSTOM_STATIC_LINK_0_NAME="Legal Notices" \
OG_CUSTOM_STATIC_LINK_0_PATH=legal.html \
OG_CUSTOM_STATIC_LINK_1_NAME=Gitea \
OG_CUSTOM_STATIC_LINK_1_PATH=https://gitea.com \
./opengist
```
## Templating custom HTML pages
In the start and end of the custom HTML files, you can use the syntax to include the header and footer of the Opengist instance:
```html
{{ template "header" . }}
<!-- my content -->
{{ template "footer" . }}
```

View File

@@ -0,0 +1,38 @@
# Run Opengist in development mode
## With Docker
Assuming you have [Make](https://linux.die.net/man/1/make) installed,
```shell
# Clone the repository
git clone git@github.com:thomiceli/opengist.git
cd opengist
# Build the development image
make build_dev_docker
```
Now you can run the development image with the following command:
```shell
make run_dev_docker
```
Opengist is now running on port 6157, you can browse http://localhost:6157
## As a binary
Requirements:
* [Git](https://git-scm.com/downloads) (2.28+)
* [Go](https://go.dev/doc/install) (1.21+)
* [Node.js](https://nodejs.org/en/download/) (16+)
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
```shell
git clone git@github.com:thomiceli/opengist.git
cd opengist
make watch
```
Opengist is now running on port 6157, you can browse http://localhost:6157

View File

@@ -11,13 +11,15 @@ Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
* Create public, unlisted or private snippets
* [Init](/docs/usage/init-via-git.md) / Clone / Pull / Push snippets **via Git** over HTTP or SSH
* Revisions history
* Syntax highlighting ; markdown & CSV support
* Search code in snippets ; browse users snippets, likes and forks
* Embed snippets in other websites
* Revisions history
* Like / Fork snippets
* Search for snippets ; browse users snippets, likes and forks
* Editor with indentation mode & size ; drag and drop files
* Download raw files or as a ZIP archive
* OAuth2 login with GitHub, Gitea, and OpenID Connect
* 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
@@ -35,7 +37,7 @@ Written in [Go](https://go.dev), Opengist aims to be fast and easy to deploy.
## 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.20** or later is recommended as the app has not been tested with older Git versions.
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.

View File

@@ -47,9 +47,9 @@ Download the archive for your system from the release page [here](https://github
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.5.1/opengist1.5.1-linux-amd64.tar.gz
wget https://github.com/thomiceli/opengist/releases/download/v1.7.0/opengist1.7.0-linux-amd64.tar.gz
tar xzvf opengist1.5.1-linux-amd64.tar.gz
tar xzvf opengist1.7.0-linux-amd64.tar.gz
cd opengist
chmod +x opengist
./opengist # with or without `--config config.yml`
@@ -58,10 +58,11 @@ chmod +x opengist
## From source
Requirements :
* [Git](https://git-scm.com/downloads) (2.20+)
* [Go](https://go.dev/doc/install) (1.20+)
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+)
* [Make](https://linux.die.net/man/1/make) (optional, but easier)
```shell
git clone https://github.com/thomiceli/opengist

57
docs/update.md Normal file
View File

@@ -0,0 +1,57 @@
# Update
## Make a backup
Before updating, always make sure to backup the Opengist home directory, where all the data is stored.
You can do so by copying the `~/.opengist` directory (default location).
```shell
cp -r ~/.opengist ~/.opengist.bak
```
## Install the new version
### With Docker
Pull the last version of Opengist
```shell
docker pull ghcr.io/thomiceli/opengist:1
```
And restart the container, using `docker compose up -d` for example if you use docker compose.
### Via binary
Stop the running instance; then like your first installation of Opengist, download the archive for your system from the release page [here](https://github.com/thomiceli/opengist/releases/latest), and extract it.
```shell
# example for linux amd64
wget https://github.com/thomiceli/opengist/releases/download/v1.7.0/opengist1.7.0-linux-amd64.tar.gz
tar xzvf opengist1.7.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
View 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
View 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"
}
```

View File

@@ -0,0 +1,26 @@
# Push Options
Opengist has support for a few [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt).
These options are passed to `git push` command and can be used to change the metadata of a gist.
## Set URL
```shell
git push -o url=mygist # Will set the URL to https://opengist.example.com/user/mygist
```
## Change title
```shell
git push -o title=Gist123
git push -o title="My Gist 123"
```
## Change visibility
```shell
git push -o visibility=public
git push -o visibility=unlisted
git push -o visibility=private
```

View 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
```

88
go.mod
View File

@@ -1,54 +1,92 @@
module github.com/thomiceli/opengist
go 1.20
go 1.21
require (
github.com/glebarez/go-sqlite v1.21.2
github.com/glebarez/sqlite v1.9.0
github.com/go-playground/validator/v10 v10.15.4
github.com/google/uuid v1.3.1
github.com/gorilla/sessions v1.2.1
github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/alecthomas/chroma/v2 v2.12.0
github.com/blevesearch/bleve/v2 v2.3.10
github.com/dustin/go-humanize v1.0.1
github.com/glebarez/go-sqlite v1.22.0
github.com/glebarez/sqlite v1.10.0
github.com/go-playground/validator/v10 v10.16.0
github.com/google/uuid v1.5.0
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.2.2
github.com/hashicorp/go-memdb v1.3.4
github.com/labstack/echo/v4 v4.11.1
github.com/labstack/echo/v4 v4.11.4
github.com/markbates/goth v1.78.0
github.com/rs/zerolog v1.30.0
github.com/rs/zerolog v1.31.0
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.13.0
golang.org/x/text v0.13.0
github.com/urfave/cli/v2 v2.27.1
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
gorm.io/gorm v1.25.4
gorm.io/gorm v1.25.5
)
require (
github.com/RoaringBitmap/roaring v1.7.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/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // 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/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // 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-isatty v0.0.19 // 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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/time v0.3.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.etcd.io/bbolt v1.3.8 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
modernc.org/libc v1.24.1 // 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.1 // indirect
modernc.org/sqlite v1.25.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
)

214
go.sum
View File

@@ -34,45 +34,113 @@ 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=
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/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/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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
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/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/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/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=
@@ -101,6 +169,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
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 v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -113,6 +183,10 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
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/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=
@@ -124,21 +198,23 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
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-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/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
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.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
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/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.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.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -151,12 +227,18 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
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/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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
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.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -165,10 +247,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
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/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
@@ -177,31 +259,46 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/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/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY=
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/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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -211,16 +308,31 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
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/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -233,8 +345,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -296,16 +408,16 @@ golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81R
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -344,18 +456,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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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-20220520151302-bc2c85ada10a/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -364,13 +475,13 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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-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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -500,19 +611,18 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -520,14 +630,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-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=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
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.1 h1:9J+2/GKTlV503mk3yv8QJ6oEpRCUrRy0ad8TXEPoV8M=
modernc.org/memory v1.7.1/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

177
internal/actions/actions.go Normal file
View File

@@ -0,0 +1,177 @@
package actions
import (
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"os"
"path/filepath"
"strings"
"sync"
)
type ActionStatus struct {
Running bool
}
const (
SyncReposFromFS = iota
SyncReposFromDB
GitGcRepos
SyncGistPreviews
ResetHooks
IndexGists
)
var (
mutex sync.Mutex
actions = make(map[int]ActionStatus)
)
func updateActionStatus(actionType int, running bool) {
actions[actionType] = ActionStatus{
Running: running,
}
}
func IsRunning(actionType int) bool {
mutex.Lock()
defer mutex.Unlock()
return actions[actionType].Running
}
func Run(actionType int) {
mutex.Lock()
if actions[actionType].Running {
mutex.Unlock()
return
}
updateActionStatus(actionType, true)
mutex.Unlock()
defer func() {
mutex.Lock()
updateActionStatus(actionType, false)
mutex.Unlock()
}()
var functionToRun func()
switch actionType {
case SyncReposFromFS:
functionToRun = syncReposFromFS
case SyncReposFromDB:
functionToRun = syncReposFromDB
case GitGcRepos:
functionToRun = gitGcRepos
case SyncGistPreviews:
functionToRun = syncGistPreviews
case ResetHooks:
functionToRun = resetHooks
case IndexGists:
functionToRun = indexGists
default:
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)
}
}
}

50
internal/cli/admin.go Normal file
View File

@@ -0,0 +1,50 @@
package cli
import (
"fmt"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/utils"
"github.com/urfave/cli/v2"
)
var CmdAdmin = cli.Command{
Name: "admin",
Usage: "Admin commands",
Subcommands: []*cli.Command{
&CmdAdminResetPassword,
},
}
var CmdAdminResetPassword = cli.Command{
Name: "reset-password",
Usage: "Reset the password for a given user",
ArgsUsage: "[username] [password]",
Action: func(ctx *cli.Context) error {
initialize(ctx)
if ctx.NArg() < 2 {
return fmt.Errorf("username and password are required")
}
username := ctx.Args().Get(0)
plainPassword := ctx.Args().Get(1)
user, err := db.GetUserByUsername(username)
if err != nil {
fmt.Printf("Cannot get user %s: %s\n", username, err)
return err
}
password, err := utils.Argon2id.Hash(plainPassword)
if err != nil {
fmt.Printf("Cannot hash password for user %s: %s\n", username, err)
return err
}
user.Password = password
if err = user.Update(); err != nil {
fmt.Printf("Cannot update password for user %s: %s\n", username, err)
return err
}
fmt.Printf("Password for user %s has been reset.\n", username)
return nil
},
}

56
internal/cli/hook.go Normal file
View File

@@ -0,0 +1,56 @@
package cli
import (
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/hooks"
"github.com/urfave/cli/v2"
"io"
"os"
"path/filepath"
)
var CmdHook = cli.Command{
Name: "hook",
Usage: "Run Git server hooks, used and should only be called by Opengist itself",
Subcommands: []*cli.Command{
&CmdHookPreReceive,
&CmdHookPostReceive,
},
}
var CmdHookPreReceive = cli.Command{
Name: "pre-receive",
Usage: "Run Git server pre-receive hook for a repository",
Action: func(ctx *cli.Context) error {
initialize(ctx)
if err := hooks.PreReceive(os.Stdin, os.Stdout, os.Stderr); err != nil {
os.Exit(1)
}
return nil
},
}
var CmdHookPostReceive = cli.Command{
Name: "post-receive",
Usage: "Run Git server post-receive hook for a repository",
Action: func(ctx *cli.Context) error {
initialize(ctx)
if err := hooks.PostReceive(os.Stdin, os.Stdout, os.Stderr); err != nil {
os.Exit(1)
}
return nil
},
}
func initialize(ctx *cli.Context) {
if err := config.InitConfig(ctx.String("config"), io.Discard); err != nil {
panic(err)
}
config.InitLog()
if err := db.Setup(filepath.Join(config.GetHomeDir(), config.C.DBFilename), false); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database in hooks")
}
}

156
internal/cli/main.go Normal file
View File

@@ -0,0 +1,156 @@
package cli
import (
"fmt"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/memdb"
"github.com/thomiceli/opengist/internal/ssh"
"github.com/thomiceli/opengist/internal/web"
"github.com/urfave/cli/v2"
"os"
"path"
"path/filepath"
)
var CmdVersion = cli.Command{
Name: "version",
Usage: "Print the version of Opengist",
Action: func(c *cli.Context) error {
fmt.Println("Opengist v" + config.OpengistVersion)
return nil
},
}
var CmdStart = cli.Command{
Name: "start",
Usage: "Start Opengist server",
Action: func(ctx *cli.Context) error {
Initialize(ctx)
go web.NewServer(os.Getenv("OG_DEV") == "1").Start()
go ssh.Start()
select {}
},
}
var ConfigFlag = cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Usage: "Path to a config file in YAML format",
}
func App() error {
app := cli.NewApp()
app.Name = "Opengist"
app.Usage = "A self-hosted pastebin powered by Git."
app.HelpName = "opengist"
app.Commands = []*cli.Command{&CmdVersion, &CmdStart, &CmdHook, &CmdAdmin}
app.DefaultCommand = CmdStart.Name
app.Flags = []cli.Flag{
&ConfigFlag,
}
return app.Run(os.Args)
}
func Initialize(ctx *cli.Context) {
fmt.Println("Opengist v" + config.OpengistVersion)
if err := config.InitConfig(ctx.String("config"), os.Stdout); err != nil {
panic(err)
}
if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil {
panic(err)
}
config.InitLog()
gitVersion, err := git.GetGitVersion()
if err != nil {
log.Fatal().Err(err).Send()
}
if ok, err := config.CheckGitVersion(gitVersion); err != nil {
log.Fatal().Err(err).Send()
} else if !ok {
log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.28 and some features would not work. " +
"Current git version: " + gitVersion)
}
homePath := config.GetHomeDir()
log.Info().Msg("Data directory: " + homePath)
if err := createSymlink(homePath, ctx.String("config")); err != nil {
log.Fatal().Err(err).Msg("Failed to create symlinks")
}
if err := os.MkdirAll(filepath.Join(homePath, "sessions"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
if err := os.MkdirAll(filepath.Join(homePath, "repos"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
if err := os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
if err := os.MkdirAll(filepath.Join(homePath, "custom"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), false); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database")
}
if err := memdb.Setup(); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize in memory database")
}
if config.C.IndexEnabled {
log.Info().Msg("Index directory: " + filepath.Join(homePath, config.C.IndexDirname))
if err := index.Open(filepath.Join(homePath, config.C.IndexDirname)); err != nil {
log.Fatal().Err(err).Msg("Failed to open index")
}
}
}
func createSymlink(homePath string, configPath string) error {
if err := os.MkdirAll(filepath.Join(homePath, "symlinks"), 0755); err != nil {
return err
}
exePath, err := os.Executable()
if err != nil {
return err
}
symlinkExePath := path.Join(config.GetHomeDir(), "symlinks", "opengist")
if _, err := os.Lstat(symlinkExePath); err == nil {
if err := os.Remove(symlinkExePath); err != nil {
return err
}
}
if err = os.Symlink(exePath, symlinkExePath); err != nil {
return err
}
if configPath == "" {
return nil
}
configPath, _ = filepath.Abs(configPath)
configPath = filepath.Clean(configPath)
symlinkConfigPath := path.Join(config.GetHomeDir(), "symlinks", "config.yml")
if _, err := os.Lstat(symlinkConfigPath); err == nil {
if err := os.Remove(symlinkConfigPath); err != nil {
return err
}
}
if err = os.Symlink(configPath, symlinkConfigPath); err != nil {
return err
}
return nil
}

View File

@@ -2,10 +2,12 @@ package config
import (
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
@@ -15,7 +17,7 @@ import (
"gopkg.in/yaml.v3"
)
var OpengistVersion = "1.5.2"
var OpengistVersion = "1.7.0"
var C *config
@@ -23,9 +25,14 @@ var C *config
// doesn't support dot notation in this case sadly
type config struct {
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"`
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
DBFilename string `yaml:"db-filename" env:"OG_DB_FILENAME"`
IndexEnabled bool `yaml:"index.enabled" env:"OG_INDEX_ENABLED"`
IndexDirname string `yaml:"index.dirname" env:"OG_INDEX_DIRNAME"`
GitDefaultBranch string `yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH"`
SqliteJournalMode string `yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE"`
@@ -42,25 +49,39 @@ type config struct {
GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"`
GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"`
GitlabClientKey string `yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY"`
GitlabSecret string `yaml:"gitlab.secret" env:"OG_GITLAB_SECRET"`
GitlabUrl string `yaml:"gitlab.url" env:"OG_GITLAB_URL"`
GitlabName string `yaml:"gitlab.name" env:"OG_GITLAB_NAME"`
GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"`
GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"`
GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"`
GiteaName string `yaml:"gitea.name" env:"OG_GITEA_NAME"`
OIDCClientKey string `yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY"`
OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"`
OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"`
CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"`
CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"`
StaticLinks []StaticLink `yaml:"custom.static-links" env:"OG_CUSTOM_STATIC_LINK"`
}
type StaticLink struct {
Name string `yaml:"name" env:"OG_CUSTOM_STATIC_LINK_#_NAME"`
Path string `yaml:"path" env:"OG_CUSTOM_STATIC_LINK_#_PATH"`
}
func configWithDefaults() (*config, error) {
homeDir, err := os.UserHomeDir()
c := &config{}
if err != nil {
return c, err
}
c.LogLevel = "warn"
c.OpengistHome = filepath.Join(homeDir, ".opengist")
c.LogOutput = "stdout,file"
c.OpengistHome = ""
c.DBFilename = "opengist.db"
c.IndexEnabled = true
c.IndexDirname = "opengist.index"
c.SqliteJournalMode = "WAL"
@@ -73,32 +94,47 @@ func configWithDefaults() (*config, error) {
c.SshPort = "2222"
c.SshKeygen = "ssh-keygen"
c.GiteaUrl = "http://gitea.com"
c.GitlabName = "GitLab"
c.GiteaUrl = "https://gitea.com"
c.GiteaName = "Gitea"
return c, nil
}
func InitConfig(configPath string) error {
func InitConfig(configPath string, out io.Writer) error {
// Default values
c, err := configWithDefaults()
if err != nil {
return err
}
if err = loadConfigFromYaml(c, configPath); err != nil {
if err = loadConfigFromYaml(c, configPath, out); err != nil {
return err
}
if err = loadConfigFromEnv(c); err != nil {
if err = loadConfigFromEnv(c, out); err != nil {
return err
}
if c.OpengistHome == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("opengist home directory is not set and current user home directory could not be determined; please specify the opengist home directory manually via the configuration")
}
c.OpengistHome = filepath.Join(homeDir, ".opengist")
}
if err = checks(c); err != nil {
return err
}
C = c
if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil {
return err
}
return nil
}
@@ -106,21 +142,46 @@ func InitLog() {
if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil {
panic(err)
}
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
var level zerolog.Level
level, err = zerolog.ParseLevel(C.LogLevel)
level, err := zerolog.ParseLevel(C.LogLevel)
if err != nil {
level = zerolog.InfoLevel
}
multi := zerolog.MultiLevelWriter(zerolog.NewConsoleWriter(), file)
var logWriters []io.Writer
logOutputTypes := utils.RemoveDuplicates[string](
strings.Split(strings.ToLower(C.LogOutput), ","),
)
for _, logOutputType := range logOutputTypes {
logOutputType = strings.TrimSpace(logOutputType)
if !slices.Contains([]string{"stdout", "file"}, logOutputType) {
defer func() { log.Warn().Msg("Invalid log output type: " + logOutputType) }()
continue
}
switch logOutputType {
case "stdout":
logWriters = append(logWriters, zerolog.NewConsoleWriter())
defer func() { log.Debug().Msg("Logging to stdout") }()
case "file":
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
logWriters = append(logWriters, file)
defer func() { log.Debug().Msg("Logging to file: " + file.Name()) }()
}
}
if len(logWriters) == 0 {
logWriters = append(logWriters, zerolog.NewConsoleWriter())
defer func() { log.Warn().Msg("No valid log outputs, defaulting to stdout") }()
}
multi := zerolog.MultiLevelWriter(logWriters...)
log.Logger = zerolog.New(multi).Level(level).With().Timestamp().Logger()
if !utils.SliceContains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
if !slices.Contains([]string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, strings.ToLower(C.LogLevel)) {
log.Warn().Msg("Invalid log level: " + C.LogLevel)
}
}
@@ -139,8 +200,8 @@ func CheckGitVersion(version string) (bool, error) {
return false, fmt.Errorf("invalid minor version number")
}
// Check if version is prior to 2.20
if major < 2 || (major == 2 && minor < 20) {
// Check if version is prior to 2.28
if major < 2 || (major == 2 && minor < 28) {
return false, nil
}
return true, nil
@@ -151,7 +212,7 @@ func GetHomeDir() string {
return filepath.Clean(absolutePath)
}
func loadConfigFromYaml(c *config, configPath string) error {
func loadConfigFromYaml(c *config, configPath string, out io.Writer) error {
if configPath != "" {
absolutePath, _ := filepath.Abs(configPath)
absolutePath = filepath.Clean(absolutePath)
@@ -160,9 +221,9 @@ func loadConfigFromYaml(c *config, configPath string) error {
if !os.IsNotExist(err) {
return err
}
fmt.Println("No YAML config file found at " + absolutePath)
_, _ = fmt.Fprintln(out, "No YAML config file found at "+absolutePath)
} else {
fmt.Println("Using YAML config file: " + absolutePath)
_, _ = fmt.Fprintln(out, "Using YAML config file: "+absolutePath)
// Override default values with values from config.yml
d := yaml.NewDecoder(file)
@@ -172,13 +233,13 @@ func loadConfigFromYaml(c *config, configPath string) error {
defer file.Close()
}
} else {
fmt.Println("No YAML config file specified.")
_, _ = fmt.Fprintln(out, "No YAML config file specified.")
}
return nil
}
func loadConfigFromEnv(c *config) error {
func loadConfigFromEnv(c *config, out io.Writer) error {
v := reflect.ValueOf(c).Elem()
var envVars []string
@@ -190,28 +251,69 @@ func loadConfigFromEnv(c *config) error {
}
envValue := os.Getenv(strings.ToUpper(tag))
if envValue == "" {
if envValue == "" && v.Field(i).Kind() != reflect.Slice {
continue
}
switch v.Field(i).Kind() {
case reflect.String:
v.Field(i).SetString(envValue)
envVars = append(envVars, tag)
case reflect.Bool:
boolVal, err := strconv.ParseBool(envValue)
if err != nil {
return err
}
v.Field(i).SetBool(boolVal)
envVars = append(envVars, tag)
case reflect.Slice:
if v.Type().Field(i).Type.Elem().Kind() == reflect.Struct {
prefix := strings.ToUpper(tag) + "_"
var sliceValue reflect.Value
elemType := v.Type().Field(i).Type.Elem()
for index := 0; ; index++ {
allFieldsPresent := true
elemValue := reflect.New(elemType).Elem()
for j := 0; j < elemValue.NumField() && allFieldsPresent; j++ {
elemField := elemValue.Type().Field(j)
envName := fmt.Sprintf("%s%d_%s", prefix, index, strings.ToUpper(elemField.Name))
envValue, present := os.LookupEnv(envName)
if !present {
allFieldsPresent = false
break
}
envVars = append(envVars, envName)
elemValue.Field(j).SetString(envValue)
}
if !allFieldsPresent {
break
}
if sliceValue.Kind() != reflect.Slice {
sliceValue = reflect.MakeSlice(v.Type().Field(i).Type, 0, index+1)
}
sliceValue = reflect.Append(sliceValue, elemValue)
}
if sliceValue.IsValid() {
v.Field(i).Set(sliceValue)
}
}
default:
return fmt.Errorf("unsupported type: %s", v.Field(i).Kind())
}
envVars = append(envVars, tag)
}
if len(envVars) > 0 {
fmt.Println("Using environment variables config: " + strings.Join(envVars, ", "))
_, _ = fmt.Fprintln(out, "Using environment variables config: "+strings.Join(envVars, ", "))
} else {
fmt.Println("No environment variables config specified.")
_, _ = fmt.Fprintln(out, "No environment variables config specified.")
}
return nil

View File

@@ -2,13 +2,13 @@ package db
import (
"errors"
"slices"
"strings"
msqlite "github.com/glebarez/go-sqlite"
"github.com/glebarez/sqlite"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/utils"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
@@ -19,7 +19,7 @@ func Setup(dbPath string, sharedCache bool) error {
var err error
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
if !utils.SliceContains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
if !slices.Contains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
}
@@ -42,7 +42,7 @@ func Setup(dbPath string, sharedCache bool) error {
return err
}
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil {
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}); err != nil {
return err
}
@@ -80,3 +80,12 @@ func IsUniqueConstraintViolation(err error) bool {
}
return false
}
func Ping() error {
sql, err := db.DB()
if err != nil {
return err
}
return sql.Ping()
}

View File

@@ -1,22 +1,62 @@
package db
import (
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/git"
"gorm.io/gorm"
"fmt"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/dustin/go-humanize"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/index"
"os/exec"
"path/filepath"
"strings"
"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", "public":
return PublicVisibility, nil
case "1", "unlisted":
return UnlistedVisibility, nil
case "2", "private":
return PrivateVisibility, nil
default:
return -1, fmt.Errorf("unknown visibility %q", s)
}
}
type Gist struct {
ID uint `gorm:"primaryKey"`
Uuid string
Title string
URL string
Preview string
PreviewFilename string
Description string
Private int // 0: public, 1: unlisted, 2: private
Private Visibility // 0: public, 1: unlisted, 2: private
UserID uint
User User
NbFiles int
@@ -48,7 +88,7 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
func GetGist(user string, gistUuid string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").
Where("gists.uuid = ? AND users.username like ?", gistUuid, user).
Where("(gists.uuid = ? OR gists.url = ?) AND users.username like ?", gistUuid, gistUuid, user).
Joins("join users on gists.user_id = users.id").
First(&gist).Error
@@ -177,6 +217,25 @@ func GetAllGistsRows() ([]*Gist, error) {
return gists, err
}
func GetAllGistsVisibleByUser(userId uint) ([]uint, error) {
var gists []uint
err := db.Table("gists").
Where("gists.private = 0 or gists.user_id = ?", userId).
Pluck("gists.id", &gists).Error
return gists, err
}
func GetAllGistsByIds(ids []uint) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
Where("id in ?", ids).
Find(&gists).Error
return gists, err
}
func (gist *Gist) Create() error {
// avoids foreign key constraint error because the default value in the struct is 0
return db.Omit("forked_id").Create(&gist).Error
@@ -190,6 +249,10 @@ func (gist *Gist) Update() error {
return db.Omit("forked_id").Save(&gist).Error
}
func (gist *Gist) UpdateNoTimestamps() error {
return db.Omit("forked_id", "updated_at").Save(&gist).Error
}
func (gist *Gist) Delete() error {
err := gist.DeleteRepository()
if err != nil {
@@ -266,33 +329,29 @@ func (gist *Gist) InitRepository() error {
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 {
return git.DeleteRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) Files(revision string) ([]*git.File, error) {
var files []*git.File
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
func (gist *Gist) Files(revision string, truncate bool) ([]*git.File, error) {
filesCat, err := git.CatFileBatch(gist.User.Username, gist.Uuid, revision, truncate)
if err != nil {
// if the revision or the file do not exist
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
return nil, nil
return nil, &git.RevisionNotFoundError{}
}
return nil, err
}
for _, fileStr := range filesStr {
file, err := gist.File(revision, fileStr, true)
if err != nil {
return nil, err
}
files = append(files, file)
var files []*git.File
for _, fileCat := range filesCat {
files = append(files, &git.File{
Filename: fileCat.Name,
Size: fileCat.Size,
HumanSize: humanize.IBytes(fileCat.Size),
Content: fileCat.Content,
Truncated: fileCat.Truncated,
})
}
return files, err
}
@@ -305,13 +364,26 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*git.Fi
return nil, nil
}
var size uint64
size, err = git.GetFileSize(gist.User.Username, gist.Uuid, revision, filename)
if err != nil {
return nil, err
}
return &git.File{
Filename: filename,
Size: size,
HumanSize: humanize.IBytes(size),
Content: content,
Truncated: truncated,
}, err
}
func (gist *Gist) FileNames(revision string) ([]string, error) {
return git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
}
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
return git.GetLog(gist.User.Username, gist.Uuid, skip)
}
@@ -321,7 +393,7 @@ func (gist *Gist) NbCommits() (string, 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
}
@@ -342,6 +414,26 @@ func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
return git.Push(gist.Uuid)
}
func (gist *Gist) AddAndCommitFile(file *FileDTO) error {
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email, false); err != nil {
return err
}
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
return err
}
if err := git.AddAll(gist.Uuid); err != nil {
return err
}
if err := git.CommitRepository(gist.Uuid, gist.User.Username, gist.User.Email); err != nil {
return err
}
return git.Push(gist.Uuid)
}
func (gist *Gist) ForkClone(username string, uuid string) error {
return git.ForkClone(gist.User.Username, gist.Uuid, username, uuid)
}
@@ -354,7 +446,7 @@ func (gist *Gist) RPC(service string) ([]byte, error) {
return git.RPC(gist.User.Username, gist.Uuid, service)
}
func (gist *Gist) UpdatePreviewAndCount() error {
func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
if err != nil {
return err
@@ -380,18 +472,66 @@ func (gist *Gist) UpdatePreviewAndCount() error {
gist.PreviewFilename = file.Filename
}
return gist.Update()
if withTimestampUpdate {
return gist.Update()
}
return gist.UpdateNoTimestamps()
}
func (gist *Gist) VisibilityStr() string {
switch gist.Private {
case PublicVisibility:
return "public"
case UnlistedVisibility:
return "unlisted"
case PrivateVisibility:
return "private"
default:
return ""
}
}
func (gist *Gist) Identifier() string {
if gist.URL != "" {
return gist.URL
}
return gist.Uuid
}
func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
files, err := gist.Files("HEAD", true)
if err != nil {
return nil, err
}
languages := make([]string, 0, len(files))
for _, file := range files {
var lexer chroma.Lexer
if lexer = lexers.Get(file.Filename); lexer == nil {
lexer = lexers.Fallback
}
fileType := lexer.Config().Name
if lexer.Config().Name == "fallback" || lexer.Config().Name == "plaintext" {
fileType = "Text"
}
languages = append(languages, fileType)
}
return languages, nil
}
// -- DTO -- //
type GistDTO struct {
Title string `validate:"max=250" form:"title"`
Description string `validate:"max=1000" form:"description"`
Private int `validate:"number,min=0,max=2" form:"private"`
Files []FileDTO `validate:"min=1,dive"`
Name []string `form:"name"`
Content []string `form:"content"`
Title string `validate:"max=250" form:"title"`
Description string `validate:"max=1000" form:"description"`
URL string `validate:"max=32,alphanumdashorempty" form:"url"`
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 {
@@ -404,11 +544,84 @@ func (dto *GistDTO) ToGist() *Gist {
Title: dto.Title,
Description: dto.Description,
Private: dto.Private,
URL: dto.URL,
}
}
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
gist.Title = dto.Title
gist.Description = dto.Description
gist.URL = dto.URL
return gist
}
// -- Index -- //
func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
files, err := gist.Files("HEAD", true)
if err != nil {
return nil, err
}
exts := make([]string, 0, len(files))
wholeContent := ""
for _, file := range files {
wholeContent += file.Content
exts = append(exts, filepath.Ext(file.Filename))
}
fileNames, err := gist.FileNames("HEAD")
if err != nil {
return nil, err
}
langs, err := gist.GetLanguagesFromFiles()
if err != nil {
return nil, err
}
indexedGist := &index.Gist{
GistID: gist.ID,
Username: gist.User.Username,
Title: gist.Title,
Content: wholeContent,
Filenames: fileNames,
Extensions: exts,
Languages: langs,
CreatedAt: gist.CreatedAt,
UpdatedAt: gist.UpdatedAt,
}
return indexedGist, nil
}
func (gist *Gist) AddInIndex() {
if !index.Enabled() {
return
}
go func() {
indexedGist, err := gist.ToIndexedGist()
if err != nil {
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
return
}
err = index.AddInIndex(indexedGist)
if err != nil {
log.Error().Err(err).Msgf("Error adding gist %d to index", gist.ID)
}
}()
}
func (gist *Gist) RemoveFromIndex() {
if !index.Enabled() {
return
}
go func() {
err := index.RemoveFromIndex(gist.ID)
if err != nil {
log.Error().Err(err).Msgf("Error remove gist %d from index", gist.ID)
}
}()
}

87
internal/db/invitation.go Normal file
View File

@@ -0,0 +1,87 @@
package db
import (
"math/rand"
"time"
)
type Invitation struct {
ID uint `gorm:"primaryKey"`
Code string
ExpiresAt int64
NbUsed uint
NbMax uint
}
func GetAllInvitations() ([]*Invitation, error) {
var invitations []*Invitation
err := db.
Order("(((expires_at >= strftime('%s', 'now')) AND ((nb_max <= 0) OR (nb_used < nb_max)))) desc").
Order("id asc").
Find(&invitations).Error
return invitations, err
}
func GetInvitationByID(id uint) (*Invitation, error) {
invitation := new(Invitation)
err := db.
Where("id = ?", id).
First(&invitation).Error
return invitation, err
}
func GetInvitationByCode(code string) (*Invitation, error) {
invitation := new(Invitation)
err := db.
Where("code = ?", code).
First(&invitation).Error
return invitation, err
}
func InvitationCodeExists(code string) (bool, error) {
var count int64
err := db.Model(&Invitation{}).Where("code = ?", code).Count(&count).Error
return count > 0, err
}
func (i *Invitation) Create() error {
i.Code = generateRandomCode()
return db.Create(&i).Error
}
func (i *Invitation) Update() error {
return db.Save(&i).Error
}
func (i *Invitation) Delete() error {
return db.Delete(&i).Error
}
func (i *Invitation) IsExpired() bool {
return i.ExpiresAt < time.Now().Unix()
}
func (i *Invitation) IsMaxedOut() bool {
return i.NbMax > 0 && i.NbUsed >= i.NbMax
}
func (i *Invitation) IsUsable() bool {
return !i.IsExpired() && !i.IsMaxedOut()
}
func (i *Invitation) Use() error {
i.NbUsed++
return i.Update()
}
func generateRandomCode() string {
const charset = "0123456789ABCDEF"
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
result := make([]byte, 16)
for i := range result {
result[i] = charset[seededRand.Intn(len(charset))]
}
return string(result)
}

View File

@@ -19,7 +19,7 @@ type SSHKey struct {
User User `validate:"-" `
}
func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error {
func (sshKey *SSHKey) BeforeCreate(*gorm.DB) error {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
if err != nil {
return err

View File

@@ -14,6 +14,7 @@ type User struct {
MD5Hash string // for gravatar, if no Email is specified, the value is random
AvatarURL string
GithubID string
GitlabID string
GiteaID string
OIDCID string `gorm:"column:oidc_id"`
@@ -52,6 +53,11 @@ func (user *User) BeforeDelete(tx *gorm.DB) error {
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
}
@@ -100,7 +106,6 @@ func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error)
err := db.
Where("email IN ?", emails).
Find(&users).Error
if err != nil {
return nil, err
}
@@ -129,6 +134,8 @@ func GetUserByProvider(id string, provider string) (*User, error) {
switch provider {
case "github":
err = db.Where("github_id = ?", id).First(&user).Error
case "gitlab":
err = db.Where("gitlab_id = ?", id).First(&user).Error
case "gitea":
err = db.Where("gitea_id = ?", id).First(&user).Error
case "openid-connect":
@@ -167,20 +174,16 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
}
func (user *User) DeleteProviderID(provider string) error {
switch provider {
case "github":
providerIDFields := map[string]string{
"github": "github_id",
"gitlab": "gitlab_id",
"gitea": "gitea_id",
"openid-connect": "oidc_id",
}
if providerIDField, ok := providerIDFields[provider]; ok {
return db.Model(&user).
Update("github_id", nil).
Update("avatar_url", nil).
Error
case "gitea":
return db.Model(&user).
Update("gitea_id", nil).
Update("avatar_url", nil).
Error
case "openid-connect":
return db.Model(&user).
Update("oidc_id", nil).
Update(providerIDField, nil).
Update("avatar_url", nil).
Error
}
@@ -191,7 +194,7 @@ func (user *User) DeleteProviderID(provider string) error {
// -- DTO -- //
type UserDTO struct {
Username string `form:"username" validate:"required,max=24,alphanum,notreserved"`
Username string `form:"username" validate:"required,max=24,alphanumdash,notreserved"`
Password string `form:"password" validate:"required"`
}

View File

@@ -1,17 +1,22 @@
package git
import (
"bufio"
"bytes"
"context"
"fmt"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
)
var (
@@ -20,6 +25,12 @@ var (
const truncateLimit = 2 << 18
type RevisionNotFoundError struct{}
func (m *RevisionNotFoundError) Error() string {
return "revision not found"
}
func RepositoryPath(user string, gist string) string {
return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist)
}
@@ -53,28 +64,20 @@ func TmpRepositoriesPath() string {
func InitRepository(user string, gist string) error {
repositoryPath := RepositoryPath(user, gist)
cmd := exec.Command(
"git",
"init",
"--bare",
repositoryPath,
)
var args []string
args = append(args, "init")
if config.C.GitDefaultBranch != "" {
args = append(args, "--initial-branch", config.C.GitDefaultBranch)
}
args = append(args, "--bare", repositoryPath)
cmd := exec.Command("git", args...)
if err := cmd.Run(); err != nil {
return err
}
return createDotGitFiles(repositoryPath)
}
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))
return CreateDotGitFiles(user, gist)
}
func CountCommits(user string, gist string) (string, error) {
@@ -113,6 +116,120 @@ func GetFilesOfRepository(user string, gist string, revision string) ([]string,
return slice[:len(slice)-1], nil
}
type catFileBatch struct {
Name, Hash, Content string
Size uint64
Truncated bool
}
func CatFileBatch(user string, gist string, revision string, truncate bool) ([]*catFileBatch, error) {
repositoryPath := RepositoryPath(user, gist)
lsTreeCmd := exec.Command("git", "ls-tree", "-l", revision)
lsTreeCmd.Dir = repositoryPath
lsTreeOutput, err := lsTreeCmd.Output()
if err != nil {
return nil, err
}
fileMap := make([]*catFileBatch, 0)
lines := strings.Split(string(lsTreeOutput), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 4 {
continue // Skip lines that don't have enough fields
}
hash := fields[2]
size, err := strconv.ParseUint(fields[3], 10, 64)
if err != nil {
continue // Skip lines with invalid size field
}
name := strings.Join(fields[4:], " ") // File name may contain spaces
fileMap = append(fileMap, &catFileBatch{
Hash: hash,
Size: size,
Name: name,
})
}
catFileCmd := exec.Command("git", "cat-file", "--batch")
catFileCmd.Dir = repositoryPath
stdin, err := catFileCmd.StdinPipe()
if err != nil {
return nil, err
}
stdout, err := catFileCmd.StdoutPipe()
if err != nil {
return nil, err
}
if err = catFileCmd.Start(); err != nil {
return nil, err
}
reader := bufio.NewReader(stdout)
for _, file := range fileMap {
_, err = stdin.Write([]byte(file.Hash + "\n"))
if err != nil {
return nil, err
}
header, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
parts := strings.Fields(header)
if len(parts) > 3 {
continue // Not a valid header, skip this entry
}
size, err := strconv.ParseUint(parts[2], 10, 64)
if err != nil {
return nil, err
}
sizeToRead := size
if truncate && sizeToRead > truncateLimit {
sizeToRead = truncateLimit
}
// Read exactly size bytes from header, or the max allowed if truncated
content := make([]byte, sizeToRead)
if _, err = io.ReadFull(reader, content); err != nil {
return nil, err
}
file.Content = string(content)
if truncate && size > truncateLimit {
// skip other bytes if truncated
if _, err = reader.Discard(int(size - truncateLimit)); err != nil {
return nil, err
}
file.Truncated = true
}
// Read the blank line following the content
if _, err := reader.ReadByte(); err != nil {
return nil, err
}
}
if err = stdin.Close(); err != nil {
return nil, err
}
if err = catFileCmd.Wait(); err != nil {
return nil, err
}
return fileMap, nil
}
func GetFileContent(user string, gist string, revision string, filename string, truncate bool) (string, bool, error) {
repositoryPath := RepositoryPath(user, gist)
@@ -121,7 +238,12 @@ func GetFileContent(user string, gist string, revision string, filename string,
maxBytes = truncateLimit
}
cmd := exec.Command(
// Set up a context with a timeout
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
cmd := exec.CommandContext(
ctx,
"git",
"--no-pager",
"show",
@@ -129,22 +251,36 @@ func GetFileContent(user string, gist string, revision string, filename string,
)
cmd.Dir = repositoryPath
stdout, _ := cmd.StdoutPipe()
err := cmd.Start()
output, err := cmd.Output()
if err != nil {
return "", false, err
}
output, truncated, err := truncateCommandOutput(stdout, maxBytes)
content, truncated, err := truncateCommandOutput(bytes.NewReader(output), maxBytes)
if err != nil {
return "", false, err
}
if err := cmd.Wait(); 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 output, truncated, nil
return strconv.ParseUint(strings.TrimSuffix(string(stdout), "\n"), 10, 64)
}
func GetLog(user string, gist string, skip int) ([]*Commit, error) {
@@ -180,7 +316,7 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) {
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)
tmpPath := TmpRepositoriesPath()
@@ -198,11 +334,13 @@ func CloneTmp(user string, gist string, gistTmpId string, email string) error {
return err
}
// remove every file (and not the .git directory!)
if err = removeFilesExceptGit(tmpRepositoryPath); err != nil {
return err
// remove every file (keep the .git directory)
// useful when user wants to edit multiple files from an existing gist
if remove {
if err = removeFilesExceptGit(tmpRepositoryPath); err != nil {
return err
}
}
cmd = exec.Command("git", "config", "--local", "user.name", user)
cmd.Dir = tmpRepositoryPath
if err = cmd.Run(); err != nil {
@@ -223,7 +361,7 @@ func ForkClone(userSrc string, gistSrc string, userDst string, gistDst string) e
return err
}
return createDotGitFiles(repositoryPathDst)
return CreateDotGitFiles(userDst, gistDst)
}
func SetFileContent(gistTmpId string, filename string, content string) error {
@@ -276,7 +414,6 @@ func Push(gistTmpId string) error {
if err != nil {
return err
}
return os.RemoveAll(tmpRepositoryPath)
}
@@ -377,22 +514,28 @@ func GetGitVersion() (string, error) {
return versionFields[2], nil
}
func createDotGitFiles(repositoryPath string) error {
func CreateDotGitFiles(user string, gist string) error {
repositoryPath := RepositoryPath(user, gist)
f1, err := os.OpenFile(filepath.Join(repositoryPath, "git-daemon-export-ok"), os.O_RDONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
defer f1.Close()
if err = createDotGitHookFile(repositoryPath, "pre-receive", preReceive); err != nil {
return err
if os.Getenv("OPENGIST_SKIP_GIT_HOOKS") != "1" {
for _, hook := range []string{"pre-receive", "post-receive"} {
if err = createDotGitHookFile(repositoryPath, hook, fmt.Sprintf(hookTemplate, hook)); err != nil {
return err
}
}
}
return nil
}
func createDotGitHookFile(repositoryPath string, hook string, content string) error {
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744)
preReceiveDst, err := os.OpenFile(filepath.Join(repositoryPath, "hooks", hook), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0744)
if err != nil {
return err
}
@@ -420,51 +563,6 @@ func removeFilesExceptGit(dir string) error {
})
}
const preReceive = `#!/bin/sh
disallowed_files=""
while read -r old_rev new_rev ref
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
do
case $file in
*/*)
disallowed_files="${disallowed_files}${file} "
;;
esac
done <<EOF
$changed_files
EOF
done
if [ -n "$disallowed_files" ]; then
echo ""
echo "Pushing files in folders is not allowed:"
for file in $disallowed_files; do
echo " $file"
done
echo ""
exit 1
fi
`
const postReceive = `#!/bin/sh
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
const hookTemplate = `#!/bin/sh
"$OG_OPENGIST_HOME_INTERNAL/symlinks/opengist" --config=$OG_OPENGIST_HOME_INTERNAL/symlinks/config.yml hook %s
`

View File

@@ -1,42 +1,18 @@
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)
SetupTest(t)
defer TeardownTest(t)
cmd := exec.Command("git", "rev-parse", "--is-bare-repository")
cmd.Dir = RepositoryPath("thomas", "gist1")
@@ -44,9 +20,6 @@ func TestInitDeleteRepository(t *testing.T) {
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")
@@ -56,14 +29,14 @@ func TestInitDeleteRepository(t *testing.T) {
}
func TestCommits(t *testing.T) {
setup(t)
defer teardown(t)
SetupTest(t)
defer TeardownTest(t)
hasNoCommits, err := HasNoCommits("thomas", "gist1")
require.NoError(t, err, "Could not check if repository has no commits")
require.True(t, hasNoCommits, "Repository should have no commits")
commitToBare(t, "thomas", "gist1", nil)
CommitToBare(t, "thomas", "gist1", nil)
hasNoCommits, err = HasNoCommits("thomas", "gist1")
require.NoError(t, err, "Could not check if repository has no commits")
@@ -73,17 +46,17 @@ func TestCommits(t *testing.T) {
require.NoError(t, err, "Could not count commits")
require.Equal(t, "1", nbCommits, "Repository should have 1 commit")
commitToBare(t, "thomas", "gist1", nil)
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)
SetupTest(t)
defer TeardownTest(t)
commitToBare(t, "thomas", "gist1", map[string]string{
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "I love Opengist\n",
"my_other_file.txt": `I really
hate Opengist`,
@@ -104,7 +77,7 @@ hate Opengist`,
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{
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_renamed_file.txt": "I love Opengist\n",
"my_other_file.txt": `I really
like Opengist actually`,
@@ -183,18 +156,18 @@ like Opengist actually`,
}
func TestGitGc(t *testing.T) {
setup(t)
defer teardown(t)
SetupTest(t)
defer TeardownTest(t)
err := GcRepos()
require.NoError(t, err, "Could not run git gc")
}
func TestFork(t *testing.T) {
setup(t)
defer teardown(t)
SetupTest(t)
defer TeardownTest(t)
commitToBare(t, "thomas", "gist1", map[string]string{
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "I love Opengist\n",
})
@@ -211,10 +184,10 @@ func TestFork(t *testing.T) {
}
func TestTruncate(t *testing.T) {
setup(t)
defer teardown(t)
SetupTest(t)
defer TeardownTest(t)
commitToBare(t, "thomas", "gist1", map[string]string{
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "A",
})
@@ -228,7 +201,7 @@ func TestTruncate(t *testing.T) {
builder.WriteString("A")
}
str := builder.String()
commitToBare(t, "thomas", "gist1", map[string]string{
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": str,
})
@@ -237,7 +210,7 @@ func TestTruncate(t *testing.T) {
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{
CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "AA\n" + str,
})
@@ -247,52 +220,23 @@ func TestTruncate(t *testing.T) {
require.Equal(t, 2, len(content), "Content size is not correct")
}
func TestInitViaGitInit(t *testing.T) {
setup(t)
defer teardown(t)
func TestGitInitBranchNames(t *testing.T) {
SetupTest(t)
defer TeardownTest(t)
e := echo.New()
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")
// 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)
config.C.GitDefaultBranch = "main"
err = InitRepository("thomas", "gist2")
require.NoError(t, err)
}
func commitToBare(t *testing.T, user string, gist string, files map[string]string) {
err := CloneTmp(user, gist, gist, "thomas@mail.com")
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")
}
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")
}

View File

@@ -11,12 +11,14 @@ import (
)
type File struct {
Filename string
OldFilename string
Content string
Truncated bool
IsCreated bool
IsDeleted bool
Filename string `json:"filename"`
Size uint64 `json:"size"`
HumanSize string `json:"human_size"`
OldFilename string `json:"-"`
Content string `json:"content"`
Truncated bool `json:"truncated"`
IsCreated bool `json:"-"`
IsDeleted bool `json:"-"`
}
type CsvFile struct {
@@ -92,6 +94,11 @@ func parseLog(out io.Reader, maxBytes int) []*Commit {
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 scanner.Bytes()[0] != ' ' {
commits = append(commits, currentCommit)

View File

@@ -0,0 +1,71 @@
package git
import (
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
)
func SetupTest(t *testing.T) {
_ = os.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1")
err := config.InitConfig("", io.Discard)
require.NoError(t, err, "Could not init config")
err = os.MkdirAll(path.Join(config.GetHomeDir(), "tests"), 0755)
ReposDirectory = path.Join("tests")
require.NoError(t, err)
err = os.MkdirAll(filepath.Join(config.GetHomeDir(), "tmp", "repos"), 0755)
require.NoError(t, err)
err = InitRepository("thomas", "gist1")
require.NoError(t, err)
}
func TeardownTest(t *testing.T) {
err := os.RemoveAll(path.Join(config.GetHomeDir(), "tests"))
require.NoError(t, err, "Could not remove repos directory")
}
func CommitToBare(t *testing.T, user string, gist string, files map[string]string) {
err := CloneTmp(user, gist, gist, "thomas@mail.com", true)
require.NoError(t, err, "Could not clone repository")
if len(files) > 0 {
for filename, content := range files {
if strings.Contains(filename, "/") {
dir := filepath.Dir(filename)
err := os.MkdirAll(filepath.Join(TmpRepositoryPath(gist), dir), os.ModePerm)
require.NoError(t, err, "Could not create directory")
}
_ = os.WriteFile(filepath.Join(TmpRepositoryPath(gist), filename), []byte(content), 0644)
if err := AddAll(gist); err != nil {
require.NoError(t, err, "Could not add all to repository")
}
}
}
if err := CommitRepository(gist, user, "thomas@mail.com"); err != nil {
require.NoError(t, err, "Could not commit to repository")
}
if err := Push(gist); err != nil {
require.NoError(t, err, "Could not push to repository")
}
}
func LastHashOfCommit(t *testing.T, user string, gist string) string {
cmd := exec.Command("git", "rev-parse", "HEAD")
cmd.Dir = RepositoryPath(user, gist)
out, err := cmd.Output()
require.NoError(t, err, "Could not run git command")
return strings.TrimSpace(string(out))
}

24
internal/hooks/hook.go Normal file
View File

@@ -0,0 +1,24 @@
package hooks
import (
"fmt"
"os"
"strconv"
"strings"
)
const BaseHash = "0000000000000000000000000000000000000000"
func pushOptions() map[string]string {
opts := make(map[string]string)
if pushCount, err := strconv.Atoi(os.Getenv("GIT_PUSH_OPTION_COUNT")); err == nil {
for i := 0; i < pushCount; i++ {
opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", i))
kv := strings.SplitN(opt, "=", 2)
if len(kv) == 2 {
opts[kv[0]] = kv[1]
}
}
}
return opts
}

View File

@@ -0,0 +1,107 @@
package hooks
import (
"bufio"
"fmt"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/utils"
"io"
"os"
"os/exec"
"slices"
"strings"
)
func PostReceive(in io.Reader, out, er io.Writer) error {
var outputSb strings.Builder
newGist := false
opts := pushOptions()
gistUrl := os.Getenv("OPENGIST_REPOSITORY_URL_INTERNAL")
validator := utils.NewValidator()
scanner := bufio.NewScanner(in)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) != 3 {
_, _ = fmt.Fprintln(er, "Invalid input")
return fmt.Errorf("invalid input")
}
oldrev, _, refname := parts[0], parts[1], parts[2]
if err := verifyHEAD(); err != nil {
setSymbolicRef(refname)
}
if oldrev == BaseHash {
newGist = true
}
}
gist, err := db.GetGistByID(os.Getenv("OPENGIST_REPOSITORY_ID"))
if err != nil {
_, _ = fmt.Fprintln(er, "Failed to get gist")
return fmt.Errorf("failed to get gist: %w", err)
}
if slices.Contains([]string{"public", "unlisted", "private"}, opts["visibility"]) {
gist.Private, _ = db.ParseVisibility(opts["visibility"])
outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"]))
}
if opts["url"] != "" && validator.Var(opts["url"], "max=32,alphanumdashorempty") == nil {
gist.URL = opts["url"]
lastIndex := strings.LastIndex(gistUrl, "/")
gistUrl = gistUrl[:lastIndex+1] + gist.URL
if !newGist {
outputSb.WriteString(fmt.Sprintf("Gist URL set to %s. Set the Git remote URL via:\n", gistUrl))
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
}
}
if opts["title"] != "" && validator.Var(opts["title"], "max=250") == nil {
gist.Title = opts["title"]
outputSb.WriteString(fmt.Sprintf("Gist title set to \"%s\"\n\n", opts["title"]))
}
if hasNoCommits, err := git.HasNoCommits(gist.User.Username, gist.Uuid); err != nil {
_, _ = fmt.Fprintln(er, "Failed to check if gist has no commits")
return fmt.Errorf("failed to check if gist has no commits: %w", err)
} else if hasNoCommits {
if err = gist.Delete(); err != nil {
_, _ = fmt.Fprintln(er, "Failed to delete gist")
return fmt.Errorf("failed to delete gist: %w", err)
}
}
_ = gist.SetLastActiveNow()
err = gist.UpdatePreviewAndCount(true)
if err != nil {
_, _ = fmt.Fprintln(er, "Failed to update gist")
return fmt.Errorf("failed to update gist: %w", err)
}
gist.AddInIndex()
if newGist {
outputSb.WriteString(fmt.Sprintf("Your new gist has been created here: %s\n", gistUrl))
outputSb.WriteString("If you want to keep working with your gist, you could set the Git remote URL via:\n")
outputSb.WriteString(fmt.Sprintf("git remote set-url origin %s\n\n", gistUrl))
}
outputStr := outputSb.String()
if outputStr != "" {
_, _ = fmt.Fprint(out, "\n"+outputStr)
}
return nil
}
func verifyHEAD() error {
return exec.Command("git", "rev-parse", "--verify", "--quiet", "HEAD").Run()
}
func setSymbolicRef(refname string) {
_ = exec.Command("git", "symbolic-ref", "HEAD", refname).Run()
}

View File

@@ -0,0 +1,78 @@
package hooks
import (
"bufio"
"bytes"
"fmt"
"io"
"os/exec"
"strings"
)
func PreReceive(in io.Reader, out, er io.Writer) error {
var err error
var disallowedFiles []string
var disallowedCommits []string
scanner := bufio.NewScanner(in)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, " ")
if len(parts) < 3 {
_, _ = fmt.Fprintln(er, "Invalid input")
return fmt.Errorf("invalid input")
}
oldRev, newRev := parts[0], parts[1]
var changedFiles string
if oldRev == BaseHash {
// First commit
if changedFiles, err = getChangedFiles(newRev); err != nil {
_, _ = fmt.Fprintln(er, "Failed to get changed files")
return err
}
} else {
if changedFiles, err = getChangedFiles(fmt.Sprintf("%s..%s", oldRev, newRev)); err != nil {
_, _ = fmt.Fprintln(er, "Failed to get changed files")
return err
}
}
var currentCommit string
for _, file := range strings.Fields(changedFiles) {
if strings.HasPrefix(file, "/") {
currentCommit = file[1:]
}
if strings.Contains(file[1:], "/") {
disallowedFiles = append(disallowedFiles, file)
disallowedCommits = append(disallowedCommits, currentCommit[0:7])
}
}
}
if len(disallowedFiles) > 0 {
_, _ = fmt.Fprintln(out, "\nPushing files in directories is not allowed:")
for i := range disallowedFiles {
_, _ = fmt.Fprintf(out, " %s (%s)\n", disallowedFiles[i], disallowedCommits[i])
}
_, _ = fmt.Fprintln(out)
return fmt.Errorf("pushing files in directories is not allowed: %s", disallowedFiles)
}
return nil
}
func getChangedFiles(rev string) (string, error) {
cmd := exec.Command("git", "log", "--name-only", "--format=/%H", "--diff-filter=AM", rev)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", err
}
return out.String(), nil
}

View File

@@ -0,0 +1,54 @@
package hooks
import (
"bytes"
"fmt"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/git"
"os"
"testing"
)
func TestPreReceiveHook(t *testing.T) {
git.SetupTest(t)
defer git.TeardownTest(t)
var lastCommitHash string
err := os.Chdir(git.RepositoryPath("thomas", "gist1"))
require.NoError(t, err, "Could not change directory")
git.CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "some allowed file",
"my_file2.txt": "some allowed file\nagain",
})
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
require.NoError(t, err, "Should not have an error on pre-receive hook for commit+push 1")
git.CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "some allowed file",
"dir/my_file.txt": "some disallowed file suddenly",
})
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
require.Error(t, err, "Should have an error on pre-receive hook for commit+push 2")
require.Equal(t, "pushing files in directories is not allowed: [dir/my_file.txt]", err.Error(), "Error message is not correct")
git.CommitToBare(t, "thomas", "gist1", map[string]string{
"my_file.txt": "some allowed file",
"dir/ok/afileagain.txt": "some disallowed file\nagain",
})
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
require.Error(t, err, "Should have an error on pre-receive hook for commit+push 3")
require.Equal(t, "pushing files in directories is not allowed: [dir/ok/afileagain.txt dir/my_file.txt]", err.Error(), "Error message is not correct")
git.CommitToBare(t, "thomas", "gist1", map[string]string{
"allowedfile.txt": "some allowed file only",
})
lastCommitHash = git.LastHashOfCommit(t, "thomas", "gist1")
err = PreReceive(bytes.NewBufferString(fmt.Sprintf("%s %s %s", BaseHash, lastCommitHash, "refs/heads/master")), os.Stdout, os.Stderr)
require.Error(t, err, "Should have an error on pre-receive hook for commit+push 4")
require.Equal(t, "pushing files in directories is not allowed: [dir/ok/afileagain.txt dir/my_file.txt]", err.Error(), "Error message is not correct")
_ = os.Chdir(os.TempDir()) // Leave the current dir to avoid errors on teardown
}

View File

@@ -53,6 +53,8 @@ func (store *LocaleStore) loadLocaleFromYAML(localeCode, path string) error {
name := display.Self.Name(tag)
if tag == language.AmericanEnglish {
name = "English"
} else if tag == language.EuropeanSpanish {
name = "Español"
}
locale := &Locale{

View File

@@ -0,0 +1,183 @@
gist.public: Veřejný
gist.unlisted: Neveřejný
gist.private: Privátní
gist.header.like: To se mi líbí
gist.header.unlike: Už se mi nelíbí
gist.header.fork: Fork
gist.header.edit: Upravit
gist.header.delete: Smazat
gist.header.forked-from: Forkováno z
gist.header.last-active: Naposledy aktivní
gist.header.select-tab: Vyberte záložku
gist.header.code: Kód
gist.header.revisions: Revize
gist.header.revision: Revize
gist.header.clone-http: Klonovat pomocí %s
gist.header.clone-http-help: Klonovat s pomocí Git pomocí základní autentizace HTTP.
gist.header.clone-ssh: Klonovat pomocí SSH
gist.header.clone-ssh-help: Klonovat s pomocí Git pomocí klíče SSH.
gist.header.embed:
gist.header.embed-help:
gist.header.download-zip: Stáhnout ZIP
gist.raw: Raw
gist.file-truncated: Tento soubor byl zkrácen.
gist.watch-full-file: Zobrazit celý soubor.
gist.file-not-valid: Tento soubor není validní CSV.
gist.no-content: Žádný obsah
gist.new.new_gist: Nový gist
gist.new.title: Titulek
gist.new.description: Popis
gist.new.filename-with-extension: Název s příponou
gist.new.indent-mode: Režim odsazení
gist.new.indent-mode-space: Mezery
gist.new.indent-mode-tab: Tabulátory
gist.new.indent-size: Velikost odsazení
gist.new.wrap-mode: Režim zalamování
gist.new.wrap-mode-no: Bez zalamování
gist.new.wrap-mode-soft: Měkké zalamování
gist.new.add-file: Přidat soubor
gist.new.create-public-button: Vytvořit veřejný gist
gist.new.create-unlisted-button: Vytvořit neveřejný gist
gist.new.create-private-button: Vytvořit soukromý gist
gist.edit.editing: Úprava
gist.edit.change-visibility: Změnit viditelnost
gist.edit.delete: Smazat
gist.edit.cancel: Zrušit
gist.edit.save: Uložit
gist.list.joined: Připojeno
gist.list.all: Všechny gisty
gist.list.search-results: Výsledky hledání
gist.list.sort: Seřadit
gist.list.sort-by-created: Vytvořeno
gist.list.sort-by-updated: Aktualizováno
gist.list.order-by-asc: Nejméně nedávno
gist.list.order-by-desc: Nedávno
gist.list.select-tab: Vyberte záložku
gist.list.liked: Líbí se
gist.list.likes: Lajky
gist.list.forked: Forkováno
gist.list.forked-from: Forkováno z
gist.list.forks: Forky
gist.list.files: Soubory
gist.list.last-active: Naposledy aktivní
gist.list.no-gists: Žádné gisty
gist.forks: Forky
gist.forks.view: Zobrazit forky
gist.forks.no: Žádné veřejné forky
gist.likes: Lajky
gist.likes.no: Zatím žádné lajky
gist.revisions: Revize
gist.revision.revised: revidoval tento gist
gist.revision.go-to-revision: Přejít na revizi
gist.revision.file-created: vytvořil soubor
gist.revision.file-deleted: smazal soubor
gist.revision.file-renamed: přejmenováno na
gist.revision.diff-truncated: Diff je příliš velký na zobrazení
gist.revision.file-renamed-no-changes: Soubor přejmenován beze změn
gist.revision.empty-file: Prázdný soubor
gist.revision.no-changes: Žádné změny
gist.revision.no-revisions: Žádné revize k zobrazení
settings: Nastavení
settings.email: Email
settings.email-help: Používá se pro commity a Gravatary
settings.email-set: Nastavit email
settings.link-accounts: Propojit účty
settings.link-github-account: Propojit účet na GitHubu
settings.link-gitea-account: Propojit účet na Gitea
settings.unlink-github-account: Odpojit účet na GitHubu
settings.unlink-gitea-account: Odpojit účet na Gitea
settings.delete-account: Smazat účet
settings.delete-account-confirm: Opravdu chcete smazat svůj účet?
settings.add-ssh-key: Přidat SSH klíč
settings.add-ssh-key-help: Používá se pouze k tahání/pushování gistů pomocí Gitu přes SSH
settings.add-ssh-key-title: Titulek
settings.add-ssh-key-content: Obsah
settings.delete-ssh-key: Smazat
settings.delete-ssh-key-confirm: Potvrdit smazání SSH klíče
settings.ssh-key-added-at: Přidáno
settings.ssh-key-never-used: Nikdy nepoužito
settings.ssh-key-last-used: Naposledy použito
settings.create-password: Vytvořit heslo
settings.create-password-help: Vytvořte si heslo pro přihlášení do Opengist pomocí HTTP
settings.change-password: Změnit heslo
settings.change-password-help: Změňte své heslo pro přihlášení do Opengist pomocí HTTP
settings.password-label-title: Heslo
auth.signup-disabled: Správce zakázal registraci
auth.login: Přihlásit se
auth.signup: Registrovat
auth.new-account: Nový účet
auth.username: Uživatelské jméno
auth.password: Heslo
auth.register-instead: Raději se zaregistrovat
auth.login-instead: Raději se přihlásit
auth.oauth: Pokračovat s účtem na %s
error: Chyba
header.menu.all: Všechno
header.menu.new: Nové
header.menu.search: Hledat
header.menu.my-gists: Moje gisty
header.menu.liked: Lajknuté
header.menu.admin: Administrace
header.menu.settings: Nastavení
header.menu.logout: Odhlásit se
header.menu.register: Registrovat
header.menu.login: Přihlásit se
header.menu.light: Světlý
header.menu.dark: Tmavý
header.menu.system: Systém
footer.powered-by: Vytvořeno pomocí %s
pagination.older: Starší
pagination.newer: Novější
pagination.previous: Předchozí
pagination.next: Další
admin.admin_panel: Administrační panel
admin.general: Obecné
admin.users: Uživatelé
admin.gists: Gisty
admin.configuration: Konfigurace
admin.versions: Verze
admin.ssh_keys: SSH klíče
admin.stats: Statistiky
admin.actions: Akce
admin.actions.sync-fs: Synchronizovat gisty ze souborového systému
admin.actions.sync-db: Synchronizovat gisty z databáze
admin.actions.git-gc: Garbage collect git repozitářů
admin.id: ID
admin.user: Uživatel
admin.delete: Smazat
admin.created_at: Vytvořeno
admin.config-link: Tato konfigurace může být %s pomocí YAML konfiguračního souboru a/nebo prostřednictvím proměnných prostředí.
admin.config-link-overriden: přepsána
admin.disable-signup: Zakázat registraci
admin.disable-signup_help: Zakázat vytváření nových účtů.
admin.require-login: Vyžadovat přihlášení
admin.require-login_help: Vynutit, aby uživatelé byli přihlášeni k zobrazení gistů.
admin.disable-login: Zakázat přihlášení
admin.disable-login_help: Zakázat přihlašování pomocí formuláře pro přihlášení a vynutit používání OAuth poskytovatele.
admin.disable-gravatar: Zakázat Gravatar
admin.disable-gravatar_help: Zakázat použití Gravataru jako poskytovatele avatara.
admin.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?

View File

@@ -0,0 +1,198 @@
gist.public: 'Öffentlich'
gist.unlisted: 'Nicht Gelistet'
gist.private: 'Privat'
gist.header.like: 'Favorisieren'
gist.header.unlike: 'Favorit entfernen'
gist.header.fork: 'Fork'
gist.header.edit: 'Bearbeiten'
gist.header.delete: 'Löschen'
gist.header.forked-from: 'Geforkt von'
gist.header.last-active: 'Zuletzt aktiv'
gist.header.select-tab: 'Tab auswählen'
gist.header.code: 'Code'
gist.header.revisions: 'Änderungen'
gist.header.revision: 'Änderung'
gist.header.clone-http: 'Mit %s Klonen'
gist.header.clone-http-help: 'Mit Git und HTTP Basic Authentication Klonen.'
gist.header.clone-ssh: 'Mit SSH Klonen'
gist.header.clone-ssh-help: 'Mit Git und SSH Schlüssel Klonen.'
gist.header.embed: 'Einbetten'
gist.header.embed-help: 'Bette diese Gist in deine Webseite ein.'
gist.header.download-zip: 'ZIP Herunterladen'
gist.raw: 'Orginalformat'
gist.file-truncated: 'Diese Datei wurde abgeschnitten.'
gist.watch-full-file: 'Die gesamte Datei anzeigen.'
gist.file-not-valid: 'Diese Datei ist keine korrekte CSV Datei.'
gist.no-content: 'Keine Dateien gefunden'
gist.new.new_gist: 'Neue Gist'
gist.new.title: 'Titel'
gist.new.description: 'Beschreibung'
gist.new.url: 'URL'
gist.new.filename-with-extension: 'Dateiname mit Erweiterung'
gist.new.indent-mode: 'Einrückungs Modus'
gist.new.indent-mode-space: 'Leerzeichen'
gist.new.indent-mode-tab: 'Tab'
gist.new.indent-size: 'Einrückungs Größe'
gist.new.wrap-mode: 'Textumbruch Modus'
gist.new.wrap-mode-no: 'kein Textumruch'
gist.new.wrap-mode-soft: 'weicher Zeilenumbruch'
gist.new.add-file: 'Datei hinzufügen'
gist.new.create-public-button: 'Öffentliche Gist erstellen'
gist.new.create-unlisted-button: 'Nicht gelistete Gist erstellen'
gist.new.create-private-button: 'Private Gist erstellen'
gist.edit.editing: 'Bearbeiten'
gist.edit.change-visibility: 'Sichtbarkeit ändern'
gist.edit.delete: 'Löschen'
gist.edit.cancel: 'Abbrechen'
gist.edit.save: 'Speichern'
gist.list.joined: 'Gemeinsam'
gist.list.all: 'Alle Gists'
gist.list.search-results: 'Suchergebnisse'
gist.list.sort: 'Sortieren'
gist.list.sort-by-created: 'erstellt'
gist.list.sort-by-updated: 'bearbeitet'
gist.list.order-by-asc: 'Älteste'
gist.list.order-by-desc: 'Neueste'
gist.list.select-tab: 'Tab Auswählen'
gist.list.liked: 'Favorisiert'
gist.list.likes: 'Favoriten'
gist.list.forked: 'Forked'
gist.list.forked-from: 'Forked von'
gist.list.forks: 'Forks'
gist.list.files: 'Dateien'
gist.list.last-active: 'Zuletzt aktiv'
gist.list.no-gists: 'Keine Gists'
gist.search.found: 'Gists gefunden'
gist.search.no-results: 'Keine Gists gefunden'
gist.search.help.user: 'Gists erstellt von Nutzer'
gist.search.help.title: 'Gists mit Titel'
gist.search.help.filename: 'Gists mit Dateinamen'
gist.search.help.extension: 'Gists mit Dateiendung'
gist.search.help.language: 'Gists in Sprache'
gist.forks: 'Forks'
gist.forks.view: 'Fork ansehen'
gist.forks.no: 'Keine öffentlichen Forks'
gist.likes: 'Favoriten'
gist.likes.no: 'Keine Favorisierungen'
gist.revisions: 'Revisionen'
gist.revision.revised: 'hat die Gist bearbeitet'
gist.revision.go-to-revision: 'Zu Änderung gehen'
gist.revision.file-created: 'Datei erstellt'
gist.revision.file-deleted: 'Datei gelöscht'
gist.revision.file-renamed: 'umbenannt zu'
gist.revision.diff-truncated: 'Diff zu groß um angezeigt zu werden'
gist.revision.file-renamed-no-changes: 'Datei ohne Änderung umbenannt'
gist.revision.empty-file: 'Leere Datei'
gist.revision.no-changes: 'Keine Änderungen'
gist.revision.no-revisions: 'Keine Änderungen zum Anzeigen'
settings: 'Einstellungen'
settings.email: 'Email'
settings.email-help: 'Für Commits und Gravatar genutzt'
settings.email-set: 'Email setzen'
settings.link-accounts: 'Accounts verlinken'
settings.link-github-account: 'GitHub-Account verlinken'
settings.link-gitlab-account: 'GitLab-Account verlinken'
settings.link-gitea-account: 'Gitea-Account verlinken'
settings.unlink-github-account: 'Github-Account Verlinkung aufheben'
settings.unlink-gitlab-account: 'GitLab-Account Verlinkung aufheben'
settings.unlink-gitea-account: 'Gitea-Account Verlinkung aufheben'
settings.delete-account: 'Account löschen'
settings.delete-account-confirm: 'Bist du dir sicher, dass du den Account löschen willst?'
settings.add-ssh-key: 'SSH-Schlüssel hinzufügen'
settings.add-ssh-key-help: 'Wird nur zum Pullen/Pushen von Gists mit Git über SSH genutzt'
settings.add-ssh-key-title: 'Titel'
settings.add-ssh-key-content: 'Schlüssel'
settings.delete-ssh-key: 'Löschen'
settings.delete-ssh-key-confirm: 'Entfernen von SSH-Schlüssel bestätigen'
settings.ssh-key-added-at: 'Hinzugefügt'
settings.ssh-key-never-used: 'Nie benutzt'
settings.ssh-key-last-used: 'Zuletzt benutzt'
settings.change-username: 'Benutzername ändern'
settings.create-password: 'Password erstellen'
settings.create-password-help: 'Passwort erstellen'
settings.change-password: 'Passwort ändern'
settings.change-password-help: 'Passwort ändern'
settings.password-label-title: 'Passwort'
auth.signup-disabled: 'Das Registrieren wurde vom Administrator deaktiviert'
auth.login: 'Anmeldung'
auth.signup: 'Registration'
auth.new-account: 'Neuer Account'
auth.username: 'Benutzername'
auth.password: 'Passwort'
auth.register-instead: 'Stattdessen registrieren'
auth.login-instead: 'Stattdessen anmelden'
auth.github-oauth: 'Mit GitHub-Account fortfahren'
auth.gitlab-oauth: 'Mit GitLab-Account fortfahren'
auth.gitea-oauth: 'Mit Gitea-Account fortfahren'
error: 'Fehler'
header.menu.all: 'Alle'
header.menu.new: 'Neu'
header.menu.search: 'Suchen'
header.menu.my-gists: 'meine Gists'
header.menu.liked: 'Favorisiert'
header.menu.admin: 'Admin'
header.menu.settings: 'Einstellungen'
header.menu.logout: 'Abmelden'
header.menu.register: 'Registrieren'
header.menu.login: 'Anmelden'
header.menu.light: 'Hell'
header.menu.dark: 'Dunkel'
header.menu.system: 'System'
footer.powered-by: 'Powered by %s'
pagination.older: 'Älter'
pagination.newer: 'Neuer'
pagination.previous: 'Vorherige'
pagination.next: 'Nachfolgende'
admin.admin_panel: 'Admin Panel'
admin.general: 'Allgemein'
admin.users: 'Benutzer'
admin.gists: 'Gists'
admin.configuration: 'Konfiguration'
admin.versions: 'Versionen'
admin.ssh_keys: 'SSH Schlüssel'
admin.stats: 'Statistiken'
admin.actions: 'Aktionen'
admin.actions.sync-fs: 'Gists auf dem Dateisystem sychronisieren'
admin.actions.sync-db: 'Gists von der Datenbank synchronisieren'
admin.actions.git-gc: '„garbage collection“ bei allen git Repositories ausführen'
admin.actions.sync-previews: 'Alle Gist Vorschauen synchronisieren'
admin.actions.reset-hooks: 'Alle Git server Hooks für alle Repositories synchronisieren'
admin.actions.index-gists: 'Alle Gists Indexieren'
admin.id: 'ID'
admin.user: 'Benutzer'
admin.delete: 'Löschen'
admin.created_at: 'Erstellt'
admin.config-link: 'Diese Konfiguration kann mithilfe der YAML Konfigurationsdatei und/oder Umgebungsvariablen %s werden.'
admin.config-link-overriden: 'überschrieben'
admin.disable-signup: 'Registrierung deaktivieren'
admin.disable-signup_help: 'Die Erstellung neuer Accounts verbieten.'
admin.require-login: 'Anmeldung nötig'
admin.require-login_help: 'Benutzer müssen sich anmelden, bevor sie Gists ansehen können.'
admin.disable-login: 'Login-Maske deaktivieren'
admin.disable-login_help: 'Login über Login-Maske verbieten und Benutzung von OAuth Providern erzwingen.'
admin.disable-gravatar: 'Gravatar deaktivieren'
admin.disable-gravatar_help: 'Gravatar als Avatar-Anbieter deaktivieren.'
admin.users.delete_confirm: 'Willst du diesen Benutzer löschen?'
admin.gists.title: 'Titel'
admin.gists.private: 'privat?'
admin.gists.nb-files: 'Anz. Dateien'
admin.gists.nb-likes: 'Anz. Favoriten'
admin.gists.delete_confirm: 'Willst du diese Gist löschen?'

View File

@@ -17,19 +17,20 @@ 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.share: Share
gist.header.share-help: Copy shareable link for this gist.
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 content
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
@@ -42,6 +43,7 @@ gist.new.add-file: Add file
gist.new.create-public-button: Create public gist
gist.new.create-unlisted-button: Create unlisted gist
gist.new.create-private-button: Create private gist
gist.new.preview: Preview
gist.edit.editing: Editing
gist.edit.change-visibility: Make
@@ -67,6 +69,14 @@ 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
@@ -80,7 +90,7 @@ 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 truncated because it's too large to be shown
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
@@ -92,8 +102,10 @@ 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 ?
@@ -106,6 +118,12 @@ 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
@@ -115,8 +133,7 @@ auth.username: Username
auth.password: Password
auth.register-instead: Register instead
auth.login-instead: Login instead
auth.github-oauth: Continue with GitHub account
auth.gitea-oauth: Continue with Gitea account
auth.oauth: Continue with %s account
error: Error
@@ -145,13 +162,18 @@ admin.general: General
admin.users: Users
admin.gists: Gists
admin.configuration: Configuration
admin.invitations: Invitations
admin.invitations.create: Create invitation
admin.versions: Versions
admin.ssh_keys: SSH keys
admin.stats: Stats
admin.actions: Actions
admin.actions.sync-fs: Synchronize gists from filesystem
admin.actions.sync-db: Synchronize gists from database
admin.actions.git-gc: Garbage collect git repositories
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
@@ -175,3 +197,11 @@ admin.gists.private: Private ?
admin.gists.nb-files: Nb. files
admin.gists.nb-likes: Nb. likes
admin.gists.delete_confirm: Do you want to delete this gist ?
admin.invitations.help: Invitations can be used to create an account even if signing up is disabled.
admin.invitations.max_uses: Max uses
admin.invitations.expires_at: Expires at
admin.invitations.code: Code
admin.invitations.copy_link: Copy link
admin.invitations.uses: Uses
admin.invitations.expired: Expired

View File

@@ -0,0 +1,176 @@
gist.public: Público
gist.unlisted: No listado
gist.private: Privado
gist.header.like: Me gusta
gist.header.unlike: No me gusta
gist.header.fork: Bifurcar
gist.header.edit: Editar
gist.header.delete: Eliminar
gist.header.forked-from: Bifurcado desde
gist.header.last-active: Última actividad
gist.header.select-tab: Seleccionar pestaña
gist.header.code: Código
gist.header.revisions: Revisiones
gist.header.revision: Revisión
gist.header.clone-http: Clonar via %s
gist.header.clone-http-help: Clonar con Git usando autenticación básica HTTP.
gist.header.clone-ssh: Clonar via SSH
gist.header.clone-ssh-help: Clonar con Git usando una clave SSH.
gist.header.embed:
gist.header.embed-help:
gist.header.download-zip: Descargar ZIP
gist.raw: Sin formato
gist.file-truncated: Este archivo ha sido truncado.
gist.watch-full-file: Ver el archivo completo.
gist.file-not-valid: Este archivo no es un archivo CSV válido.
gist.no-content: Sin contenido
gist.new.new_gist: Nuevo gist
gist.new.title: Título
gist.new.description: Descripción
gist.new.filename-with-extension: Nombre de archivo con extensión
gist.new.indent-mode: Modo de sangrado
gist.new.indent-mode-space: Espacio
gist.new.indent-mode-tab: Tabulación
gist.new.indent-size: Tamaño de sangrado
gist.new.wrap-mode: Modo de ajuste
gist.new.wrap-mode-no: Sin ajuste
gist.new.wrap-mode-soft: Ajuste suave
gist.new.add-file: Agregar archivo
gist.new.create-public-button: Crear gist público
gist.new.create-unlisted-button: Crear gist no listado
gist.new.create-private-button: Crear gist privado
gist.edit.editing: Editando
gist.edit.change-visibility: Hacer
gist.edit.delete: Eliminar
gist.edit.cancel: Cancelar
gist.edit.save: Guardar
gist.list.joined: Unido
gist.list.all: Todos los gists
gist.list.search-results: Resultados de búsqueda
gist.list.sort: Ordenar
gist.list.sort-by-created: creado
gist.list.sort-by-updated: actualizado
gist.list.order-by-asc: Menos reciente
gist.list.order-by-desc: Recientemente
gist.list.select-tab: Seleccionar pestaña
gist.list.liked: Gustado
gist.list.likes: gustos
gist.list.forked: Bifurcado
gist.list.forked-from: Bifurcado desde
gist.list.forks: bifurcaciones
gist.list.files: archivos
gist.list.last-active: Última actividad
gist.list.no-gists: Sin gists
gist.forks: Bifurcaciones
gist.forks.view: Ver bifurcación
gist.forks.no: No hay bifurcaciones públicas
gist.likes: Gustos
gist.likes.no: Aún no hay gustos
gist.revisions: Revisiones
gist.revision.revised: revisó este gist
gist.revision.go-to-revision: Ir a la revisión
gist.revision.file-created: archivo creado
gist.revision.file-deleted: archivo eliminado
gist.revision.file-renamed: renombrado a
gist.revision.diff-truncated: Diferencia truncada porque es demasiado grande para mostrarse.
gist.revision.file-renamed-no-changes: Archivo renombrado sin cambios
gist.revision.empty-file: Archivo vacío
gist.revision.no-changes: Sin cambios
gist.revision.no-revisions: No hay revisiones para mostrar
settings: Configuración
settings.email: Correo electrónico
settings.email-help: Usado para confirmaciones y Gravatar
settings.email-set: Establecer correo electrónico
settings.link-accounts: Enlazar cuentas
settings.link-github-account: Enlazar cuenta de GitHub
settings.link-gitea-account: Enlazar cuenta de Gitea
settings.unlink-github-account: Desenlazar cuenta de GitHub
settings.unlink-gitea-account: Desenlazar cuenta de Gitea
settings.delete-account: Eliminar cuenta
settings.delete-account-confirm: ¿Estás seguro de que quieres eliminar tu cuenta?
settings.add-ssh-key: Agregar clave SSH
settings.add-ssh-key-help: Usado solo para extraer/push gists usando Git a través de SSH
settings.add-ssh-key-title: Título
settings.add-ssh-key-content: Clave
settings.delete-ssh-key: Eliminar
settings.delete-ssh-key-confirm: Confirmar eliminación de clave SSH
settings.ssh-key-added-at: Añadido
settings.ssh-key-never-used: Nunca usado
settings.ssh-key-last-used: Último uso
auth.signup-disabled: El administrador ha deshabilitado el registro
auth.login: Iniciar sesión
auth.signup: Registrarse
auth.new-account: Nueva cuenta
auth.username: Nombre de usuario
auth.password: Contraseña
auth.register-instead: Registrarse en su lugar
auth.login-instead: Iniciar sesión en su lugar
auth.oauth: Continuar con cuenta de %s
error: Error
header.menu.all: Todos
header.menu.new: Nuevo
header.menu.search: Buscar
header.menu.my-gists: Mis gists
header.menu.liked: Gustados
header.menu.admin: Administrador
header.menu.settings: Configuración
header.menu.logout: Cerrar sesión
header.menu.register: Registrarse
header.menu.login: Iniciar sesión
header.menu.light: Claro
header.menu.dark: Oscuro
header.menu.system: Sistema
footer.powered-by: Desarrollado por %s
pagination.older: Anterior
pagination.newer: Siguiente
pagination.previous: Anterior
pagination.next: Siguiente
admin.admin_panel: Panel de administración
admin.general: General
admin.users: Usuarios
admin.gists: Gists
admin.configuration: Configuración
admin.versions: Versiones
admin.ssh_keys: Claves SSH
admin.stats: Estadísticas
admin.actions: Acciones
admin.actions.sync-fs: Sincronizar gists desde el sistema de archivos
admin.actions.sync-db: Sincronizar gists desde la base de datos
admin.actions.git-gc: Recolectar basura en los repositorios Git
admin.id: ID
admin.user: Usuario
admin.delete: Eliminar
admin.created_at: Creado
admin.config-link: Esta configuración puede ser %s por un archivo de configuración YAML y/o variables de entorno.
admin.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?

View File

@@ -17,15 +17,15 @@ 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.share: Partager
gist.header.share-help: Copier le lien partageable de ce gist.
gist.header.embed: Intégrer
gist.header.embed-help: Intégrer ce gist dans une page web.
gist.header.download-zip: Télécharger en ZIP
gist.raw: Brut
gist.file-truncated: Ce fichier a été tronqué.
gist.watch-full-file: Voir le fichier complet.
gist.file-not-valid: Ce fichier n'est pas un fichier CSV valide.
gist.no-content: Pas de contenu
gist.no-content: Aucun fichier
gist.new.new_gist: Nouveau gist
gist.new.title: Titre
@@ -80,7 +80,7 @@ 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 tronquée car trop volumineuse pour être affichée
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
@@ -115,8 +115,7 @@ 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
auth.oauth: Continuer avec un compte %s
error: Erreur
@@ -175,3 +174,23 @@ admin.gists.private: Privé ?
admin.gists.nb-files: Nb. de fichiers
admin.gists.nb-likes: Nb. de j'aime
admin.gists.delete_confirm: Voulez-vous supprimer ce gist ?
gist.search.help.user: gists créés par un utilisateur
gist.search.help.title: gists avec un titre spécifique
gist.search.help.extension: gists qui ont des fichiers avec une extension spécifique
gist.search.found: gists trouvés
gist.search.help.filename: gists qui ont des fichiers avec un nom spécifique
settings.link-gitlab-account: Lier le compte GitLab
gist.search.help.language: gists qui ont des fichiers écrits en un langage spécifique
settings.change-username: Changer le nom d'utilisateur
settings.create-password: Créer un mot de passe
settings.create-password-help: Créer un mot de passe pour se connecter à Opengist via HTTP
settings.change-password: Changer le mot de passe
settings.change-password-help: Changer le mot de passe pour se connecter à Opengist via HTTP
settings.password-label-title: Mot de passe
auth.gitlab-oauth: Continuer avec un compte GitLab
admin.actions.sync-previews: Synchroniser l'aperçu des gists
admin.actions.reset-hooks: Réinitialiser les hooks de Git pour tous les dépôts
gist.new.url: URL
gist.search.no-results: Aucun gist trouvé
settings.unlink-gitlab-account: Détacher le compte GitLab
admin.actions.index-gists: Indexer tous les gists

View File

@@ -17,8 +17,8 @@ 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.share: Megosztás
gist.header.share-help: Másold ki ennek a gistnek a megosztható linkjét
gist.header.embed:
gist.header.embed-help:
gist.header.download-zip: ZIP archívum letöltése
gist.raw: Eredeti
@@ -30,6 +30,7 @@ gist.no-content: Nincs tartalom
gist.new.new_gist: Új gist
gist.new.title: Cím
gist.new.description: Leírás
gist.new.url: URL
gist.new.filename-with-extension: Fájlnév kiterjesztéssel
gist.new.indent-mode: Indentáció típusa
gist.new.indent-mode-space: Szóköz
@@ -67,6 +68,14 @@ gist.list.files: fájlok
gist.list.last-active: Utoljára aktív
gist.list.no-gists: Nincsenek gistek
gist.search.found: találat
gist.search.no-results: Nincsenek találatok
gist.search.help.user: létrehozva e felhasználó által
gist.search.help.title: gistek egyező címmel
gist.search.help.filename: gistek melyek tartalmaznak fájlt egyező névvel
gist.search.help.extension: gistek melyek tartalmaznak fájlt egyező kiterjesztéssel
gist.search.help.language: gistek melyek tartalmaznak fájlt egyező nyelvvel
gist.forks: Forkok
gist.forks.view: Fork megtekintése
gist.forks.no: Nincsenek nyilvános forkok
@@ -92,8 +101,10 @@ settings.email-help: Commitoknál és Gravatarnál van használva
settings.email-set: Email beállítása
settings.link-accounts: Fiókok összekötése
settings.link-github-account: GitHub fiók hozzáadása
settings.link-gitlab-account: GitLab fiók hozzáadása
settings.link-gitea-account: Gitea fiók hozzáadása
settings.unlink-github-account: GitHub fiók eltávolítása
settings.unlink-gitlab-account: GitLab fiók eltávolítása
settings.unlink-gitea-account: Gitea fiók eltávolítása
settings.delete-account: Fiók törlése
settings.delete-account-confirm: Biztosan törölni szeretnéd a fiókod?
@@ -106,6 +117,12 @@ settings.delete-ssh-key-confirm: Erősítsd meg az SSH kulcs törlését
settings.ssh-key-added-at: "Hozzáadva:"
settings.ssh-key-never-used: Sosem használt
settings.ssh-key-last-used: "Utoljára használva:"
settings.change-username: Felhasználónév megváltoztatása
settings.create-password: Jelszó létrehozása
settings.create-password-help: Hozz létre egy jelszót, hogy bejelentkezhess az OpenGist-be HTTP-n keresztül
settings.change-password: Jelszó megváltoztatása
settings.change-password-help: Változtasd meg a jelszót, amivel bejelentkezel az OpenGist-be HTTP-n keresztül
settings.password-label-title: Jelszó
auth.signup-disabled: Az adminisztrátor kikapcsolta a regisztrációkat
auth.login: Bejelentkezés
@@ -115,8 +132,7 @@ 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
auth.oauth: Folytatás %s fiókkal
error: Hiba
@@ -152,6 +168,9 @@ admin.actions: Műveletek
admin.actions.sync-fs: Gistek szinkronizálása a fájlrendszerrel
admin.actions.sync-db: Gistek szinkronizálása az adatbázissal
admin.actions.git-gc: Használatlan git repository-k eltávolítása
admin.actions.sync-previews: Gist előnézetek szinkronizálása
admin.actions.reset-hooks: Git server hook-ok alaphelyzetbe állítása minden repository-nál
admin.actions.index-gists: Gistek indexelése
admin.id: Azonosító
admin.user: Felhasználó
admin.delete: Törlés

View File

@@ -0,0 +1,176 @@
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.oauth: Continuar com conta do %s
error: Erro
header.menu.all: Todos
header.menu.new: Novo
header.menu.search: Buscar
header.menu.my-gists: Meus gists
header.menu.liked: Curtidos
header.menu.admin: Administrador
header.menu.settings: Configurações
header.menu.logout: Sair
header.menu.register: Registrar-se
header.menu.login: Entrar
header.menu.light: Claro
header.menu.dark: Escuro
header.menu.system: Sistema
footer.powered-by: Desenvolvido por %s
pagination.older: Anterior
pagination.newer: Próximo
pagination.previous: Anterior
pagination.next: Próximo
admin.admin_panel: Painel de administração
admin.general: Geral
admin.users: Usuários
admin.gists: Gists
admin.configuration: Configuração
admin.versions: Versões
admin.ssh_keys: Chaves SSH
admin.stats: Estatísticas
admin.actions: Ações
admin.actions.sync-fs: Sincronizar gists do sistema de arquivos
admin.actions.sync-db: Sincronizar gists do banco de dados
admin.actions.git-gc: Coletar lixo nos repositórios Git
admin.id: ID
admin.user: Usuário
admin.delete: Excluir
admin.created_at: Criado
admin.config-link: Esta configuração pode ser %s por um arquivo de configuração YAML e/ou variáveis de ambiente.
admin.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?

View File

@@ -17,8 +17,8 @@ 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.share: Поделиться
gist.header.share-help: Скопировать ссылку на фрагмент.
gist.header.embed:
gist.header.embed-help:
gist.header.download-zip: Скачать ZIP-архив
gist.raw: Исходник
@@ -115,8 +115,7 @@ auth.username: Имя пользователя
auth.password: Пароль
auth.register-instead: Зарегистрироваться
auth.login-instead: Войти
auth.github-oauth: Войти с помощью доступа GitHub
auth.gitea-oauth: Войти с помощью доступа Gitea
auth.oauth: Войти с помощью доступа %s
error: Ошибка

View File

@@ -17,8 +17,8 @@ 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.share: 分享
gist.header.share-help: 为此 Gist 复制可供分享的链接。
gist.header.embed:
gist.header.embed-help:
gist.header.download-zip: 下载 ZIP
gist.raw: 原始文件
@@ -115,8 +115,7 @@ auth.username: 用户名
auth.password: 密码
auth.register-instead: 转到注册
auth.login-instead: 转到登录
auth.github-oauth: 使用 GitHub 账号继续
auth.gitea-oauth: 使用 Gitea 账号继续
auth.oauth: 使用 %s 账号继续
error: 错误

View File

@@ -0,0 +1,195 @@
gist.public: 公開
gist.unlisted: 非公開
gist.private: 私人
gist.header.like: 喜歡
gist.header.unlike: 不喜歡
gist.header.fork: 分支
gist.header.edit: 編輯
gist.header.delete: 刪除
gist.header.forked-from: 分支自
gist.header.last-active: 最後活躍
gist.header.select-tab: 選擇分頁
gist.header.code: 程式碼
gist.header.revisions: 修訂記錄
gist.header.revision: 修訂
gist.header.clone-http: 透過 %s 複製
gist.header.clone-http-help: 使用 HTTP 基本認證透過 Git 複製。
gist.header.clone-ssh: 透過 SSH 複製
gist.header.clone-ssh-help: 使用 SSH 金鑰透過 Git 複製。
gist.header.embed: 嵌入
gist.header.embed-help: 將這個 Gist 嵌入您的網站。
gist.header.download-zip: 下載 ZIP
gist.raw: 原始檔案
gist.file-truncated: 此檔案已被截斷。
gist.watch-full-file: 查看完整檔案。
gist.file-not-valid: 此檔案不是有效的 CSV 檔案。
gist.no-content: 內容為空
gist.new.new_gist: 新增 Gist
gist.new.title: 標題
gist.new.description: 描述
gist.new.url: 自定義 Gist 網址
gist.new.filename-with-extension: 含副檔名的檔案名稱
gist.new.indent-mode: 縮排模式
gist.new.indent-mode-space: 空格
gist.new.indent-mode-tab: tab
gist.new.indent-size: 縮排寬度
gist.new.wrap-mode: 換行模式
gist.new.wrap-mode-no: 不換行
gist.new.wrap-mode-soft: 自動換行
gist.new.add-file: 新增檔案
gist.new.create-public-button: 創建公開 Gist
gist.new.create-unlisted-button: 創建非公開 Gist
gist.new.create-private-button: 創建私人 Gist
gist.edit.editing: 編輯中
gist.edit.change-visibility: 更改可見性
gist.edit.delete: 刪除
gist.edit.cancel: 取消
gist.edit.save: 保存
gist.list.joined: 加入
gist.list.all: 所有 Gists
gist.list.search-results: 搜索結果
gist.list.sort: 排序
gist.list.sort-by-created: 創建
gist.list.sort-by-updated: 更新
gist.list.order-by-asc: 順序排序
gist.list.order-by-desc: 倒序排序
gist.list.select-tab: 選擇分頁
gist.list.liked: 喜歡的 Gists
gist.list.likes: 喜歡
gist.list.forked: 分支
gist.list.forked-from: 分支自
gist.list.forks: 分支
gist.list.files: 檔案
gist.list.last-active: 最後活躍
gist.list.no-gists: 沒有任何的 Gist
gist.forks: 分支
gist.forks.view: 查看分支
gist.forks.no: 沒有任何公開的分支
gist.likes: 喜歡
gist.likes.no: 目前還沒有任何人喜歡
gist.revisions: 修訂版本
gist.revision.revised: 已修改
gist.revision.go-to-revision: 還原成這個修訂版本
gist.revision.file-created: 檔案已創建
gist.revision.file-deleted: 檔案已刪除
gist.revision.file-renamed: 重命名為
gist.revision.diff-truncated: 差異太大無法顯示
gist.revision.file-renamed-no-changes: 檔案名稱與重新命名前相同
gist.revision.empty-file: 檔案為空
gist.revision.no-changes: 沒有任何變更
gist.revision.no-revisions: 沒有任何修訂版可顯示
settings: 設定
settings.email: 電子郵件
settings.email-help: 用於提交和 Gravatar
settings.email-set: 設定電子郵件
settings.link-accounts: 連結帳號
settings.link-github-account: 連結 GitHub 帳號
settings.link-gitlab-account: 連結 Gitlab 帳號
settings.link-gitea-account: 連結 Gitea 帳號
settings.unlink-github-account: 取消連結 GitHub 帳號
settings.unlink-gitlab-account: 取消連結 GitLab 帳號
settings.unlink-gitea-account: 取消連結 Gitea 帳號
settings.delete-account: 刪除帳號
settings.delete-account-confirm: 確定要刪除您的帳號嗎?
settings.add-ssh-key: 添加 SSH 金鑰
settings.add-ssh-key-help: 僅用於藉由 SSH 使用 Git 拉取/推送 Gist
settings.add-ssh-key-title: 名稱
settings.add-ssh-key-content: 金鑰
settings.delete-ssh-key: 刪除
settings.delete-ssh-key-confirm: 確認刪除 SSH 金鑰
settings.ssh-key-added-at: 添加於
settings.ssh-key-never-used: 從未使用
settings.ssh-key-last-used: 最後使用
settings.change-username: 變更使用者名稱
settings.create-password: 創建密碼
settings.create-password-help: 創建您的密碼以通過 HTTP 登錄到 Opengist
settings.change-password: 更改密碼
settings.change-password-help: 更改您的密碼以通過 HTTP 登錄到 Opengist
settings.password-label-title: 密碼
auth.signup-disabled: 管理員已禁用註冊
auth.login: 登錄
auth.signup: 註冊
auth.new-account: 新增帳號
auth.username: 使用者名稱
auth.password: 密碼
auth.register-instead: 註冊
auth.login-instead: 登錄
auth.oauth: 用 %s 帳號繼續
error: 錯誤
header.menu.all: 全部
header.menu.new: 新建
header.menu.search: 搜索
header.menu.my-gists: 我的 Gists
header.menu.liked: 喜歡的 Gists
header.menu.admin: 管理
header.menu.settings: 設定
header.menu.logout: 登出
header.menu.register: 註冊
header.menu.login: 登錄
header.menu.light: 亮色
header.menu.dark: 暗色
header.menu.system: 系統
footer.powered-by: 由 %s 提供支持
pagination.older: 下一頁
pagination.newer: 上一頁
pagination.previous: 上一頁
pagination.next: 下一頁
admin.admin_panel: 管理儀表板
admin.general: 一般
admin.users: 使用者
admin.gists: Gists
admin.configuration: 設定
admin.versions: 版本
admin.ssh_keys: SSH 金鑰
admin.stats: 統計
admin.actions: 操作
admin.actions.sync-fs: 從系統同步 Gists
admin.actions.sync-db: 從資料庫同步 Gists
admin.actions.git-gc: 清理所有的 git 儲存庫
admin.actions.sync-previews: 同步所有 Gists 預覽
admin.actions.reset-hooks: 重置 Git 伺服器所有儲存庫的 Git hooks
admin.id: ID
admin.user: 使用者
admin.delete: 刪除
admin.created_at: 創建時間
admin.config-link: 這裡的設定可以通過 YAML 配置檔案或是環境變數 %s。
admin.config-link-overriden: 覆蓋
admin.disable-signup: 關閉註冊
admin.disable-signup_help: 禁止創建新帳號。
admin.require-login: 登錄後瀏覽
admin.require-login_help: 強制使用者登錄以查看 Gist。
admin.disable-login: 關閉登錄頁面
admin.disable-login_help: 關閉通過登錄頁面登錄,強制使用 OAuth 提供者。
admin.disable-gravatar: 禁用 Gravatar
admin.disable-gravatar_help: 禁止使用 Gravatar 作為頭像提供者。
admin.users.delete_confirm: 您要刪除這個使用者嗎?
admin.gists.title: 標題
admin.gists.private: 是否為私人
admin.gists.nb-files: 檔案數
admin.gists.nb-likes: 喜歡
admin.gists.delete_confirm: 您要刪除這個 Gist 嗎?
gist.search.no-results: 沒有找到任何 Gists
gist.search.help.title: Gists 的標題
gist.search.help.filename: Gists 的檔案名稱
gist.search.help.language: Gists 的程式語言
admin.actions.index-gists: 索引所有的 Gists
gist.search.help.user: 由使用者建立的 Gists
gist.search.found: 已找到 Gists
gist.search.help.extension: Gists 的副檔名

157
internal/index/bleve.go Normal file
View 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
View 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
}

View File

@@ -0,0 +1,168 @@
package render
import (
"bufio"
"bytes"
"fmt"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"sync"
)
type RenderedFile struct {
*git.File
Type string `json:"type"`
Lines []string `json:"-"`
HTML string `json:"-"`
}
type RenderedGist struct {
*db.Gist
Lines []string
HTML string
}
func HighlightFile(file *git.File) (RenderedFile, error) {
rendered := RenderedFile{
File: file,
}
style := newStyle()
lexer := newLexer(file.Filename)
if lexer.Config().Name == "markdown" {
return MarkdownFile(file)
}
formatter := html.New(html.WithClasses(true), html.PreventSurroundingPre(true))
iterator, err := lexer.Tokenise(nil, file.Content+"\n")
if err != nil {
return rendered, err
}
htmlbuf := bytes.Buffer{}
w := bufio.NewWriter(&htmlbuf)
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
lines := make([]string, 0, len(tokensLines))
for _, tokens := range tokensLines {
iterator = chroma.Literator(tokens...)
err = formatter.Format(&htmlbuf, style, iterator)
if err != nil {
return rendered, fmt.Errorf("unable to format code: %w", err)
}
lines = append(lines, htmlbuf.String())
htmlbuf.Reset()
}
_ = w.Flush()
rendered.Lines = lines
rendered.Type = parseFileTypeName(*lexer.Config())
return rendered, err
}
func HighlightFiles(files []*git.File) []RenderedFile {
const numWorkers = 10
jobs := make(chan int, numWorkers)
renderedFiles := make([]RenderedFile, len(files))
var wg sync.WaitGroup
worker := func() {
for idx := range jobs {
rendered, err := HighlightFile(files[idx])
if err != nil {
log.Error().Err(err).Msg("Error rendering gist preview for " + files[idx].Filename)
}
renderedFiles[idx] = rendered
}
wg.Done()
}
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker()
}
for i := range files {
jobs <- i
}
close(jobs)
wg.Wait()
return renderedFiles
}
func HighlightGistPreview(gist *db.Gist) (RenderedGist, error) {
rendered := RenderedGist{
Gist: gist,
}
style := newStyle()
lexer := newLexer(gist.PreviewFilename)
if lexer.Config().Name == "markdown" {
return MarkdownGistPreview(gist)
}
formatter := html.New(html.WithClasses(true), html.PreventSurroundingPre(true))
iterator, err := lexer.Tokenise(nil, gist.Preview)
if err != nil {
return rendered, err
}
htmlbuf := bytes.Buffer{}
w := bufio.NewWriter(&htmlbuf)
tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens())
lines := make([]string, 0, len(tokensLines))
for _, tokens := range tokensLines {
iterator = chroma.Literator(tokens...)
err = formatter.Format(&htmlbuf, style, iterator)
if err != nil {
return rendered, fmt.Errorf("unable to format code: %w", err)
}
lines = append(lines, htmlbuf.String())
htmlbuf.Reset()
}
_ = w.Flush()
rendered.Lines = lines
return rendered, err
}
func parseFileTypeName(config chroma.Config) string {
fileType := config.Name
if fileType == "fallback" || fileType == "plaintext" {
return "Text"
}
return fileType
}
func newLexer(filename string) chroma.Lexer {
var lexer chroma.Lexer
if lexer = lexers.Get(filename); lexer == nil {
lexer = lexers.Fallback
}
return lexer
}
func newStyle() *chroma.Style {
var style *chroma.Style
if style = styles.Get("catppuccin-latte"); style == nil {
style = styles.Fallback
}
return style
}

119
internal/render/markdown.go Normal file
View File

@@ -0,0 +1,119 @@
package render
import (
"bufio"
"bytes"
"github.com/Kunde21/markdownfmt/v3"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
astex "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"go.abhg.dev/goldmark/mermaid"
"strconv"
)
func MarkdownGistPreview(gist *db.Gist) (RenderedGist, error) {
var buf bytes.Buffer
err := newMarkdown().Convert([]byte(gist.Preview), &buf)
return RenderedGist{
Gist: gist,
HTML: buf.String(),
}, err
}
func MarkdownFile(file *git.File) (RenderedFile, error) {
var buf bytes.Buffer
err := newMarkdown().Convert([]byte(file.Content), &buf)
return RenderedFile{
File: file,
HTML: buf.String(),
Type: "Markdown",
}, err
}
func MarkdownString(content string) (string, error) {
var buf bytes.Buffer
err := newMarkdown().Convert([]byte(content), &buf)
return buf.String(), err
}
func newMarkdown() goldmark.Markdown {
return goldmark.New(
goldmark.WithExtensions(
extension.GFM,
highlighting.NewHighlighting(
highlighting.WithStyle("catppuccin-latte"),
highlighting.WithFormatOptions(html.WithClasses(true))),
emoji.Emoji,
&mermaid.Extender{},
),
goldmark.WithParserOptions(
parser.WithASTTransformers(
util.Prioritized(&CheckboxTransformer{}, 10000),
),
),
)
}
type CheckboxTransformer struct{}
func (t *CheckboxTransformer) Transform(node *ast.Document, _ text.Reader, _ parser.Context) {
i := 1
err := ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
if _, ok := n.(*astex.TaskCheckBox); ok {
listitem := n.Parent().Parent()
listitem.SetAttribute([]byte("data-checkbox-nb"), []byte(strconv.Itoa(i)))
i += 1
}
}
return ast.WalkContinue, nil
})
if err != nil {
log.Err(err)
}
}
func Checkbox(content string, checkboxNb int) (string, error) {
buf := bytes.Buffer{}
w := bufio.NewWriter(&buf)
source := []byte(content)
markdown := markdownfmt.NewGoldmark()
reader := text.NewReader(source)
document := markdown.Parser().Parse(reader)
i := 1
err := ast.Walk(document, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
if listItem, ok := n.(*astex.TaskCheckBox); ok {
if i == checkboxNb {
listItem.IsChecked = !listItem.IsChecked
}
i += 1
}
}
return ast.WalkContinue, nil
})
if err != nil {
return "", err
}
if err = markdown.Renderer().Render(w, source, document); err != nil {
return "", err
}
_ = w.Flush()
return buf.String(), nil
}

View File

@@ -94,7 +94,8 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error {
// updatedAt is updated only if serviceType is receive-pack
if verb == "receive-pack" {
_ = gist.SetLastActiveNow()
_ = gist.UpdatePreviewAndCount()
_ = gist.UpdatePreviewAndCount(false)
gist.AddInIndex()
}
return nil

View File

@@ -64,7 +64,7 @@ func listen(serverConfig *ssh.ServerConfig) {
go func() {
sConn, channels, reqs, err := ssh.NewServerConn(nConn, serverConfig)
if err != nil {
if !(err != io.EOF && !errors.Is(err, syscall.ECONNRESET)) {
if err != io.EOF && !errors.Is(err, syscall.ECONNRESET) {
errorSsh("Failed to handshake", err)
}
return

View File

@@ -0,0 +1,76 @@
package utils
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/argon2"
"strings"
)
type Argon2ID struct {
format string
version int
time uint32
memory uint32
keyLen uint32
saltLen uint32
threads uint8
}
var Argon2id = Argon2ID{
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
version: argon2.Version,
time: 1,
memory: 64 * 1024,
keyLen: 32,
saltLen: 16,
threads: 4,
}
func (a Argon2ID) Hash(plain string) (string, error) {
salt := make([]byte, a.saltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}
hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)
return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}
func (a Argon2ID) Verify(plain, hash string) (bool, error) {
if hash == "" {
return false, nil
}
hashParts := strings.Split(hash, "$")
if len(hashParts) != 6 {
return false, errors.New("invalid hash")
}
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
if err != nil {
return false, err
}
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
if err != nil {
return false, err
}
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
if err != nil {
return false, err
}
hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))
return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
}

26
internal/utils/session.go Normal file
View File

@@ -0,0 +1,26 @@
package utils
import (
"github.com/gorilla/securecookie"
"github.com/rs/zerolog/log"
"os"
)
func ReadKey(filePath string) []byte {
key, err := os.ReadFile(filePath)
if err == nil {
return key
}
key = securecookie.GenerateRandomKey(32)
if key == nil {
log.Fatal().Msg("Failed to generate a new key for sessions")
}
err = os.WriteFile(filePath, key, 0600)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to save the key to %s", filePath)
}
return key
}

View File

@@ -1,10 +1,13 @@
package utils
func SliceContains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
func RemoveDuplicates[T string | int](sliceList []T) []T {
allKeys := make(map[T]bool)
list := []T{}
for _, item := range sliceList {
if _, value := allKeys[item]; !value {
allKeys[item] = true
list = append(list, item)
}
}
return false
return list
}

View File

@@ -0,0 +1,74 @@
package utils
import (
"github.com/go-playground/validator/v10"
"regexp"
"strings"
)
type OpengistValidator struct {
v *validator.Validate
}
func NewValidator() *OpengistValidator {
v := validator.New()
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
_ = v.RegisterValidation("alphanumdash", validateAlphaNumDash)
_ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty)
return &OpengistValidator{v}
}
func (cv *OpengistValidator) Validate(i interface{}) error {
return cv.v.Struct(i)
}
func (cv *OpengistValidator) Var(field interface{}, tag string) error {
return cv.v.Var(field, tag)
}
func ValidationMessages(err *error) string {
errs := (*err).(validator.ValidationErrors)
messages := make([]string, len(errs))
for i, e := range errs {
switch e.Tag() {
case "max":
messages[i] = e.Field() + " is too long"
case "required":
messages[i] = e.Field() + " should not be empty"
case "excludes":
messages[i] = e.Field() + " should not include a sub directory"
case "alphanum":
messages[i] = e.Field() + " should only contain alphanumeric characters"
case "alphanumdash":
case "alphanumdashorempty":
messages[i] = e.Field() + " should only contain alphanumeric characters and dashes"
case "min":
messages[i] = "Not enough " + e.Field()
case "notreserved":
messages[i] = "Invalid " + e.Field()
}
}
return strings.Join(messages, " ; ")
}
func validateReservedKeywords(fl validator.FieldLevel) bool {
name := fl.Field().String()
restrictedNames := map[string]struct{}{}
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init", "healthcheck", "preview"} {
restrictedNames[restrictedName] = struct{}{}
}
// if the name is not in the restricted names, it is valid
_, ok := restrictedNames[name]
return !ok
}
func validateAlphaNumDash(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
}
func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
}

View File

@@ -2,21 +2,13 @@ package web
import (
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/actions"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
)
var (
syncReposFromFS = false
syncReposFromDB = false
gitGcRepos = false
"time"
)
func adminIndex(ctx echo.Context) error {
@@ -50,9 +42,12 @@ func adminIndex(ctx echo.Context) error {
}
setData(ctx, "countKeys", countKeys)
setData(ctx, "syncReposFromFS", syncReposFromFS)
setData(ctx, "syncReposFromDB", syncReposFromDB)
setData(ctx, "gitGcRepos", gitGcRepos)
setData(ctx, "syncReposFromFS", actions.IsRunning(actions.SyncReposFromFS))
setData(ctx, "syncReposFromDB", actions.IsRunning(actions.SyncReposFromDB))
setData(ctx, "gitGcRepos", actions.IsRunning(actions.GitGcRepos))
setData(ctx, "syncGistPreviews", actions.IsRunning(actions.SyncGistPreviews))
setData(ctx, "resetHooks", actions.IsRunning(actions.ResetHooks))
setData(ctx, "indexGists", actions.IsRunning(actions.IndexGists))
return html(ctx, "admin_index.html")
}
@@ -123,84 +118,45 @@ func adminGistDelete(ctx echo.Context) error {
return errorRes(500, "Cannot delete this gist", err)
}
gist.RemoveFromIndex()
addFlash(ctx, "Gist has been deleted", "success")
return redirect(ctx, "/admin-panel/gists")
}
func adminSyncReposFromFS(ctx echo.Context) error {
addFlash(ctx, "Syncing repositories from filesystem...", "success")
go func() {
if syncReposFromFS {
return
}
syncReposFromFS = true
gists, err := db.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
}()
go actions.Run(actions.SyncReposFromFS)
return redirect(ctx, "/admin-panel")
}
func adminSyncReposFromDB(ctx echo.Context) error {
addFlash(ctx, "Syncing repositories from database...", "success")
go func() {
if syncReposFromDB {
return
}
syncReposFromDB = true
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
if err != nil {
log.Error().Err(err).Msg("Cannot read repos directories")
syncReposFromDB = false
return
}
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).Msg("Cannot delete repository")
syncReposFromDB = false
return
}
}
}
syncReposFromDB = false
}()
go actions.Run(actions.SyncReposFromDB)
return redirect(ctx, "/admin-panel")
}
func adminGcRepos(ctx echo.Context) error {
addFlash(ctx, "Garbage collecting repositories...", "success")
go func() {
if gitGcRepos {
return
}
gitGcRepos = true
if err := git.GcRepos(); err != nil {
log.Error().Err(err).Msg("Error garbage collecting repositories")
gitGcRepos = false
return
}
gitGcRepos = false
}()
go actions.Run(actions.GitGcRepos)
return redirect(ctx, "/admin-panel")
}
func adminSyncGistPreviews(ctx echo.Context) error {
addFlash(ctx, "Syncing Gist previews...", "success")
go actions.Run(actions.SyncGistPreviews)
return redirect(ctx, "/admin-panel")
}
func adminResetHooks(ctx echo.Context) error {
addFlash(ctx, "Resetting Git server hooks for all repositories...", "success")
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")
}
@@ -224,3 +180,59 @@ func adminSetConfig(ctx echo.Context) error {
"success": true,
})
}
func adminInvitations(ctx echo.Context) error {
setData(ctx, "title", "Invitations")
setData(ctx, "htmlTitle", "Invitations - Admin panel")
setData(ctx, "adminHeaderPage", "invitations")
var invitations []*db.Invitation
var err error
if invitations, err = db.GetAllInvitations(); err != nil {
return errorRes(500, "Cannot get invites", err)
}
setData(ctx, "invitations", invitations)
return html(ctx, "admin_invitations.html")
}
func adminInvitationsCreate(ctx echo.Context) error {
code := ctx.FormValue("code")
nbMax, err := strconv.ParseUint(ctx.FormValue("nbMax"), 10, 64)
if err != nil {
nbMax = 10
}
expiresAtUnix, err := strconv.ParseInt(ctx.FormValue("expiredAtUnix"), 10, 64)
if err != nil {
expiresAtUnix = time.Now().Unix() + 604800 // 1 week
}
invitation := &db.Invitation{
Code: code,
ExpiresAt: expiresAtUnix,
NbMax: uint(nbMax),
}
if err := invitation.Create(); err != nil {
return errorRes(500, "Cannot create invitation", err)
}
addFlash(ctx, "Invitation has been created", "success")
return redirect(ctx, "/admin-panel/invitations")
}
func adminInvitationsDelete(ctx echo.Context) error {
id, _ := strconv.ParseUint(ctx.Param("id"), 10, 64)
invitation, err := db.GetInvitationByID(uint(id))
if err != nil {
return errorRes(500, "Cannot retrieve invitation", err)
}
if err := invitation.Delete(); err != nil {
return errorRes(500, "Cannot delete this invitation", err)
}
addFlash(ctx, "Invitation has been deleted", "success")
return redirect(ctx, "/admin-panel/invitations")
}

View File

@@ -6,37 +6,68 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/labstack/echo/v4"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitea"
"github.com/markbates/goth/providers/github"
"github.com/markbates/goth/providers/gitlab"
"github.com/markbates/goth/providers/openidConnect"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/utils"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gorm.io/gorm"
"io"
"net/http"
"net/url"
"strings"
)
const (
GitHubProvider = "github"
GitLabProvider = "gitlab"
GiteaProvider = "gitea"
OpenIDConnect = "openid-connect"
)
var title = cases.Title(language.English)
func register(ctx echo.Context) error {
disableSignup := getData(ctx, "DisableSignup")
disableForm := getData(ctx, "DisableLoginForm")
code := ctx.QueryParam("code")
if code != "" {
if invitation, err := db.GetInvitationByCode(code); err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return errorRes(500, "Cannot check for invitation code", err)
} else if invitation != nil && invitation.IsUsable() {
disableSignup = false
}
}
setData(ctx, "title", tr(ctx, "auth.new-account"))
setData(ctx, "htmlTitle", "New account")
setData(ctx, "disableForm", getData(ctx, "DisableLoginForm"))
setData(ctx, "disableForm", disableForm)
setData(ctx, "disableSignup", disableSignup)
setData(ctx, "isLoginPage", false)
return html(ctx, "auth_form.html")
}
func processRegister(ctx echo.Context) error {
if getData(ctx, "DisableSignup") == true {
disableSignup := getData(ctx, "DisableSignup")
code := ctx.QueryParam("code")
invitation, err := db.GetInvitationByCode(code)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return errorRes(500, "Cannot check for invitation code", err)
} else if invitation != nil && invitation.IsUsable() {
disableSignup = false
}
if disableSignup == true {
return errorRes(403, "Signing up is disabled", nil)
}
@@ -55,7 +86,7 @@ func processRegister(ctx echo.Context) error {
}
if err := ctx.Validate(dto); err != nil {
addFlash(ctx, validationMessages(&err), "error")
addFlash(ctx, utils.ValidationMessages(&err), "error")
return html(ctx, "auth_form.html")
}
@@ -66,7 +97,7 @@ func processRegister(ctx echo.Context) error {
user := dto.ToUser()
password, err := argon2id.hash(user.Password)
password, err := utils.Argon2id.Hash(user.Password)
if err != nil {
return errorRes(500, "Cannot hash password", err)
}
@@ -82,6 +113,10 @@ func processRegister(ctx echo.Context) error {
}
}
if err := invitation.Use(); err != nil {
return errorRes(500, "Cannot use invitation", err)
}
sess.Values["user"] = user.ID
saveSession(sess, ctx)
@@ -121,7 +156,7 @@ func processLogin(ctx echo.Context) error {
return redirect(ctx, "/login")
}
if ok, err := argon2id.verify(password, user.Password); !ok {
if ok, err := utils.Argon2id.Verify(password, user.Password); !ok {
if err != nil {
return errorRes(500, "Cannot check for password", err)
}
@@ -131,6 +166,7 @@ func processLogin(ctx echo.Context) error {
}
sess.Values["user"] = user.ID
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
saveSession(sess, ctx)
deleteCsrfCookie(ctx)
@@ -140,23 +176,13 @@ func processLogin(ctx echo.Context) error {
func oauthCallback(ctx echo.Context) error {
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
if err != nil {
return errorRes(400, "Cannot complete user auth", err)
return errorRes(400, "Cannot complete user auth: "+err.Error(), err)
}
currUser := getUserLogged(ctx)
if currUser != nil {
// if user is logged in, link account to user and update its avatar URL
switch user.Provider {
case "github":
currUser.GithubID = user.UserID
currUser.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
case "gitea":
currUser.GiteaID = user.UserID
currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
case "openid-connect":
currUser.OIDCID = user.UserID
currUser.AvatarURL = user.AvatarURL
}
updateUserProviderInfo(currUser, user.Provider, user)
if err = currUser.Update(); err != nil {
return errorRes(500, "Cannot update user "+title.String(user.Provider)+" id", err)
@@ -184,17 +210,7 @@ func oauthCallback(ctx echo.Context) error {
}
// set provider id and avatar URL
switch user.Provider {
case "github":
userDB.GithubID = user.UserID
userDB.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
case "gitea":
userDB.GiteaID = user.UserID
userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
case "openid-connect":
userDB.OIDCID = user.UserID
userDB.AvatarURL = user.AvatarURL
}
updateUserProviderInfo(userDB, user.Provider, user)
if err = userDB.Create(); err != nil {
if db.IsUniqueConstraintViolation(err) {
@@ -213,11 +229,13 @@ func oauthCallback(ctx echo.Context) error {
var resp *http.Response
switch user.Provider {
case "github":
case GitHubProvider:
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
case "gitea":
case GitLabProvider:
resp, err = http.Get(urlJoin(config.C.GitlabUrl, user.NickName+".keys"))
case GiteaProvider:
resp, err = http.Get(urlJoin(config.C.GiteaUrl, user.NickName+".keys"))
case "openid-connect":
case OpenIDConnect:
err = errors.New("cannot get keys from OIDC provider")
}
@@ -273,7 +291,7 @@ func oauth(ctx echo.Context) error {
}
switch provider {
case "github":
case GitHubProvider:
goth.UseProviders(
github.New(
config.C.GithubClientKey,
@@ -282,7 +300,19 @@ func oauth(ctx echo.Context) error {
),
)
case "gitea":
case GitLabProvider:
goth.UseProviders(
gitlab.NewCustomisedURL(
config.C.GitlabClientKey,
config.C.GitlabSecret,
urlJoin(opengistUrl, "/oauth/gitlab/callback"),
urlJoin(config.C.GitlabUrl, "/oauth/authorize"),
urlJoin(config.C.GitlabUrl, "/oauth/token"),
urlJoin(config.C.GitlabUrl, "/api/v4/user"),
),
)
case GiteaProvider:
goth.UseProviders(
gitea.NewCustomisedURL(
config.C.GiteaClientKey,
@@ -293,7 +323,7 @@ func oauth(ctx echo.Context) error {
urlJoin(config.C.GiteaUrl, "/api/v1/user"),
),
)
case "openid-connect":
case OpenIDConnect:
oidcProvider, err := openidConnect.New(
config.C.OIDCClientKey,
config.C.OIDCSecret,
@@ -313,31 +343,21 @@ func oauth(ctx echo.Context) error {
currUser := getUserLogged(ctx)
if currUser != nil {
isDelete := false
var err error
switch provider {
case "github":
if currUser.GithubID != "" {
isDelete = true
err = currUser.DeleteProviderID(provider)
}
case "gitea":
if currUser.GiteaID != "" {
isDelete = true
err = currUser.DeleteProviderID(provider)
}
case "openid-connect":
if currUser.OIDCID != "" {
isDelete = true
err = currUser.DeleteProviderID(provider)
}
// Map each provider to a function that checks the relevant ID in currUser
providerIDCheckMap := map[string]func() bool{
GitHubProvider: func() bool { return currUser.GithubID != "" },
GitLabProvider: func() bool { return currUser.GitlabID != "" },
GiteaProvider: func() bool { return currUser.GiteaID != "" },
OpenIDConnect: func() bool { return currUser.OIDCID != "" },
}
if err != nil {
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
}
// Check if the provider is valid and if the user has a linked ID
// Means that the user wants to unlink the account
if checkFunc, exists := providerIDCheckMap[provider]; exists && checkFunc() {
if err := currUser.DeleteProviderID(provider); err != nil {
return errorRes(500, "Cannot unlink account from "+title.String(provider), err)
}
if isDelete {
addFlash(ctx, "Account unlinked from "+title.String(provider), "success")
return redirect(ctx, "/settings")
}
@@ -345,7 +365,7 @@ func oauth(ctx echo.Context) error {
ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider)
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
if provider != "github" && provider != "gitea" && provider != "openid-connect" {
if provider != GitHubProvider && provider != GitLabProvider && provider != GiteaProvider && provider != OpenIDConnect {
return errorRes(400, "Unsupported provider", nil)
}
@@ -368,11 +388,28 @@ func urlJoin(base string, elem ...string) string {
return joined
}
func updateUserProviderInfo(userDB *db.User, provider string, user goth.User) {
userDB.AvatarURL = getAvatarUrlFromProvider(provider, user.UserID)
switch provider {
case GitHubProvider:
userDB.GithubID = user.UserID
case GitLabProvider:
userDB.GitlabID = user.UserID
case GiteaProvider:
userDB.GiteaID = user.UserID
case OpenIDConnect:
userDB.OIDCID = user.UserID
userDB.AvatarURL = user.AvatarURL
}
}
func getAvatarUrlFromProvider(provider string, identifier string) string {
switch provider {
case "github":
case GitHubProvider:
return "https://avatars.githubusercontent.com/u/" + identifier + "?v=4"
case "gitea":
case GitLabProvider:
return urlJoin(config.C.GitlabUrl, "/uploads/-/system/user/avatar/", identifier, "/avatar.png") + "?width=400"
case GiteaProvider:
resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", identifier))
if err != nil {
log.Error().Err(err).Msg("Cannot get user from Gitea")

View File

@@ -2,18 +2,28 @@ package web
import (
"archive/zip"
"bufio"
"bytes"
"errors"
"fmt"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/render"
"github.com/thomiceli/opengist/internal/utils"
"html/template"
"net/url"
"path/filepath"
"regexp"
"strconv"
"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"
"html/template"
"net/url"
"regexp"
"strconv"
"strings"
)
func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
@@ -23,14 +33,24 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
userName := ctx.Param("user")
gistName := ctx.Param("gistname")
gistName = strings.TrimSuffix(gistName, ".git")
switch filepath.Ext(gistName) {
case ".js":
setData(ctx, "gistpage", "js")
gistName = strings.TrimSuffix(gistName, ".js")
case ".json":
setData(ctx, "gistpage", "json")
gistName = strings.TrimSuffix(gistName, ".json")
case ".git":
setData(ctx, "gistpage", "git")
gistName = strings.TrimSuffix(gistName, ".git")
}
gist, err := db.GetGist(userName, gistName)
if err != nil {
return notFound("Gist not found")
}
if gist.Private == 2 {
if gist.Private == db.PrivateVisibility {
if currUser == nil || currUser.ID != gist.UserID {
return notFound("Gist not found")
}
@@ -54,19 +74,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
}
}
httpProtocol := "http"
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
httpProtocol = "https"
}
setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))
var baseHttpUrl string
// if a custom external url is set, use it
if config.C.ExternalUrl != "" {
baseHttpUrl = config.C.ExternalUrl
} else {
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
}
baseHttpUrl := getData(ctx, "baseHttpUrl").(string)
if config.C.HttpGit {
setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
@@ -74,6 +82,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
setData(ctx, "httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
setData(ctx, "currentUrl", template.URL(ctx.Request().URL.Path))
setData(ctx, "embedScript", fmt.Sprintf(`<script src="%s"></script>`, baseHttpUrl+"/"+userName+"/"+gistName+".js"))
nbCommits, err := gist.NbCommits()
if err != nil {
@@ -231,11 +240,20 @@ func allGists(ctx echo.Context) error {
}
}
renderedGists := make([]*render.RenderedGist, 0, len(gists))
for _, gist := range gists {
rendered, err := render.HighlightGistPreview(gist)
if err != nil {
log.Error().Err(err).Msg("Error rendering gist preview for " + gist.Identifier() + " - " + gist.PreviewFilename)
}
renderedGists = append(renderedGists, &rendered)
}
if err != nil {
return errorRes(500, "Error fetching gists", err)
}
if err = paginate(ctx, gists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
if err = paginate(ctx, renderedGists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
return errorRes(404, "Page not found", nil)
}
@@ -243,7 +261,76 @@ func allGists(ctx echo.Context) error {
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 {
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")
@@ -251,27 +338,108 @@ func gistIndex(ctx echo.Context) error {
revision = "HEAD"
}
files, err := gist.Files(revision)
if err != nil {
files, err := gist.Files(revision, true)
if _, ok := err.(*git.RevisionNotFoundError); ok {
return notFound("Revision not found")
} else if err != nil {
return errorRes(500, "Error fetching files", err)
}
if len(files) == 0 {
return notFound("Revision not found")
}
renderedFiles := render.HighlightFiles(files)
setData(ctx, "page", "code")
setData(ctx, "commit", revision)
setData(ctx, "files", files)
setData(ctx, "files", renderedFiles)
setData(ctx, "revision", revision)
setData(ctx, "htmlTitle", gist.Title)
return html(ctx, "gist.html")
}
func gistJson(ctx echo.Context) error {
gist := getData(ctx, "gist").(*db.Gist)
files, err := gist.Files("HEAD", true)
if err != nil {
return errorRes(500, "Error fetching files", err)
}
renderedFiles := render.HighlightFiles(files)
setData(ctx, "files", renderedFiles)
htmlbuf := bytes.Buffer{}
w := bufio.NewWriter(&htmlbuf)
if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", dataMap(ctx), ctx); err != nil {
return err
}
_ = w.Flush()
jsUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), gist.User.Username, gist.Identifier()+".js")
if err != nil {
return errorRes(500, "Error joining js url", err)
}
cssUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), manifestEntries["embed.css"].File)
if err != nil {
return errorRes(500, "Error joining css url", err)
}
return ctx.JSON(200, map[string]interface{}{
"owner": gist.User.Username,
"id": gist.Identifier(),
"uuid": gist.Uuid,
"title": gist.Title,
"description": gist.Description,
"created_at": time.Unix(gist.CreatedAt, 0).Format(time.RFC3339),
"visibility": gist.VisibilityStr(),
"files": renderedFiles,
"embed": map[string]string{
"html": htmlbuf.String(),
"css": cssUrl,
"js": jsUrl,
"js_dark": jsUrl + "?dark",
},
})
}
func gistJs(ctx echo.Context) error {
if _, exists := ctx.QueryParams()["dark"]; exists {
setData(ctx, "dark", "dark")
}
gist := getData(ctx, "gist").(*db.Gist)
files, err := gist.Files("HEAD", true)
if err != nil {
return errorRes(500, "Error fetching files", err)
}
renderedFiles := render.HighlightFiles(files)
setData(ctx, "files", renderedFiles)
htmlbuf := bytes.Buffer{}
w := bufio.NewWriter(&htmlbuf)
if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", dataMap(ctx), ctx); err != nil {
return err
}
_ = w.Flush()
cssUrl, err := url.JoinPath(getData(ctx, "baseHttpUrl").(string), manifestEntries["embed.css"].File)
if err != nil {
return errorRes(500, "Error joining css url", err)
}
js := `document.write('<link rel="stylesheet" href="%s">')
document.write('%s')
`
content := strings.Replace(htmlbuf.String(), `\n`, `\\n`, -1)
content = strings.Replace(content, "\n", `\n`, -1)
js = fmt.Sprintf(js, cssUrl, content)
ctx.Response().Header().Set("Content-Type", "application/javascript")
return plainText(ctx, 200, js)
}
func revisions(ctx echo.Context) error {
gist := getData(ctx, "gist").(*db.Gist)
userName := gist.User.Username
gistName := gist.Uuid
gistName := gist.Identifier()
pageInt := getPage(ctx)
@@ -359,11 +527,11 @@ func processCreate(ctx echo.Context) error {
err = ctx.Validate(dto)
if err != nil {
addFlash(ctx, validationMessages(&err), "error")
addFlash(ctx, utils.ValidationMessages(&err), "error")
if isCreate {
return html(ctx, "create.html")
} else {
files, err := gist.Files("HEAD")
files, err := gist.Files("HEAD", false)
if err != nil {
return errorRes(500, "Error fetching files", err)
}
@@ -429,34 +597,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 {
var gist = getData(ctx, "gist").(*db.Gist)
gist := getData(ctx, "gist").(*db.Gist)
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)
}
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 {
var gist = getData(ctx, "gist").(*db.Gist)
gist := getData(ctx, "gist").(*db.Gist)
if err := gist.Delete(); err != nil {
return errorRes(500, "Error deleting this gist", err)
}
gist.RemoveFromIndex()
addFlash(ctx, "Gist has been deleted", "success")
return redirect(ctx, "/")
}
func like(ctx echo.Context) error {
var gist = getData(ctx, "gist").(*db.Gist)
gist := getData(ctx, "gist").(*db.Gist)
currentUser := getUserLogged(ctx)
hasLiked, err := currentUser.HasLiked(gist)
@@ -474,7 +645,7 @@ func like(ctx echo.Context) error {
return errorRes(500, "Error liking/dislking this gist", err)
}
redirectTo := "/" + gist.User.Username + "/" + gist.Uuid
redirectTo := "/" + gist.User.Username + "/" + gist.Identifier()
if r := ctx.QueryParam("redirecturl"); r != "" {
redirectTo = r
}
@@ -482,7 +653,7 @@ func like(ctx echo.Context) error {
}
func fork(ctx echo.Context) error {
var gist = getData(ctx, "gist").(*db.Gist)
gist := getData(ctx, "gist").(*db.Gist)
currentUser := getUserLogged(ctx)
alreadyForked, err := gist.GetForkParent(currentUser)
@@ -492,11 +663,11 @@ func fork(ctx echo.Context) error {
if gist.User.ID == currentUser.ID {
addFlash(ctx, "Unable to fork own gists", "error")
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid)
return redirect(ctx, "/"+gist.User.Username+"/"+gist.Identifier())
}
if alreadyForked.ID != 0 {
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Uuid)
return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Identifier())
}
uuidGist, err := uuid.NewRandom()
@@ -529,13 +700,12 @@ func fork(ctx echo.Context) error {
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 {
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)
}
@@ -550,7 +720,6 @@ func rawFile(ctx echo.Context) error {
func downloadFile(ctx echo.Context) error {
gist := getData(ctx, "gist").(*db.Gist)
file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
if err != nil {
return errorRes(500, "Error getting file content", err)
}
@@ -572,9 +741,9 @@ func downloadFile(ctx echo.Context) error {
}
func edit(ctx echo.Context) error {
var gist = getData(ctx, "gist").(*db.Gist)
gist := getData(ctx, "gist").(*db.Gist)
files, err := gist.Files("HEAD")
files, err := gist.Files("HEAD", false)
if err != nil {
return errorRes(500, "Error fetching files from repository", err)
}
@@ -586,10 +755,10 @@ func edit(ctx echo.Context) error {
}
func downloadZip(ctx echo.Context) error {
var gist = getData(ctx, "gist").(*db.Gist)
var revision = ctx.Param("revision")
gist := getData(ctx, "gist").(*db.Gist)
revision := ctx.Param("revision")
files, err := gist.Files(revision)
files, err := gist.Files(revision, false)
if err != nil {
return errorRes(500, "Error fetching files from repository", err)
}
@@ -621,7 +790,7 @@ func downloadZip(ctx echo.Context) error {
}
ctx.Response().Header().Set("Content-Type", "application/zip")
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Uuid+".zip")
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Identifier()+".zip")
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(zipFile.Bytes())))
_, err = ctx.Response().Write(zipFile.Bytes())
if err != nil {
@@ -631,7 +800,7 @@ func downloadZip(ctx echo.Context) error {
}
func likes(ctx echo.Context) error {
var gist = getData(ctx, "gist").(*db.Gist)
gist := getData(ctx, "gist").(*db.Gist)
pageInt := getPage(ctx)
@@ -640,7 +809,7 @@ func likes(ctx echo.Context) error {
return errorRes(500, "Error getting users who liked this gist", err)
}
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Uuid+"/likes", 1); err != nil {
if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Identifier()+"/likes", 1); err != nil {
return errorRes(404, "Page not found", nil)
}
@@ -650,7 +819,7 @@ func likes(ctx echo.Context) error {
}
func forks(ctx echo.Context) error {
var gist = getData(ctx, "gist").(*db.Gist)
gist := getData(ctx, "gist").(*db.Gist)
pageInt := getPage(ctx)
currentUser := getUserLogged(ctx)
@@ -664,7 +833,7 @@ func forks(ctx echo.Context) error {
return errorRes(500, "Error getting users who liked this gist", err)
}
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Uuid+"/forks", 2); err != nil {
if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Identifier()+"/forks", 2); err != nil {
return errorRes(404, "Page not found", nil)
}
@@ -672,3 +841,50 @@ func forks(ctx echo.Context) error {
setData(ctx, "revision", "HEAD")
return html(ctx, "forks.html")
}
func checkbox(ctx echo.Context) error {
filename := ctx.FormValue("file")
checkboxNb := ctx.FormValue("checkbox")
i, err := strconv.Atoi(checkboxNb)
if err != nil {
return errorRes(400, "Invalid number", nil)
}
gist := getData(ctx, "gist").(*db.Gist)
file, err := gist.File("HEAD", filename, false)
if err != nil {
return errorRes(500, "Error getting file content", err)
} else if file == nil {
return notFound("File not found")
}
markdown, err := render.Checkbox(file.Content, i)
if err != nil {
return errorRes(500, "Error checking checkbox", err)
}
if err = gist.AddAndCommitFile(&db.FileDTO{
Filename: filename,
Content: markdown,
}); err != nil {
return errorRes(500, "Error adding and committing files", err)
}
if err = gist.UpdatePreviewAndCount(true); err != nil {
return errorRes(500, "Error updating the gist", err)
}
return plainText(ctx, 200, "ok")
}
func preview(ctx echo.Context) error {
content := ctx.FormValue("content")
previewStr, err := render.MarkdownString(content)
if err != nil {
return errorRes(500, "Error rendering markdown", err)
}
return plainText(ctx, 200, previewStr)
}

View File

@@ -6,13 +6,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"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"
"github.com/thomiceli/opengist/internal/utils"
"net/http"
"os"
"os/exec"
@@ -21,6 +15,14 @@ import (
"strconv"
"strings"
"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 {
@@ -73,7 +75,7 @@ func gitHttp(ctx echo.Context) error {
// - 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 != 2 && gist.ID != 0 && !getData(ctx, "RequireLogin").(bool) {
if isPull && gist.Private != db.PrivateVisibility && gist.ID != 0 && !getData(ctx, "RequireLogin").(bool) {
return route.handler(ctx)
}
@@ -97,7 +99,7 @@ func gitHttp(ctx echo.Context) error {
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 ok, err := utils.Argon2id.Verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
if err != nil {
return errorRes(500, "Cannot verify password", err)
}
@@ -114,7 +116,7 @@ func gitHttp(ctx echo.Context) error {
return errorRes(401, "Invalid credentials", nil)
}
if ok, err := argon2id.verify(authPassword, user.Password); !ok {
if ok, err := utils.Argon2id.Verify(authPassword, user.Password); !ok {
if err != nil {
return errorRes(500, "Cannot check for password", err)
}
@@ -133,7 +135,7 @@ func gitHttp(ctx echo.Context) error {
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
gist.Title = "gist:" + gist.Uuid
if err = gist.InitRepositoryViaInit(ctx); err != nil {
if err = gist.InitRepository(); err != nil {
return errorRes(500, "Cannot init repository in the file system", err)
}
@@ -192,6 +194,7 @@ func pack(ctx echo.Context, serviceType string) error {
}
repositoryPath := getData(ctx, "repositoryPath").(string)
gist := getData(ctx, "gist").(*db.Gist)
var stderr bytes.Buffer
cmd := exec.Command("git", serviceType, "--stateless-rpc", repositoryPath)
@@ -199,25 +202,14 @@ func pack(ctx echo.Context, serviceType string) error {
cmd.Stdin = reqBody
cmd.Stdout = ctx.Response().Writer
cmd.Stderr = &stderr
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "OPENGIST_REPOSITORY_URL_INTERNAL="+git.RepositoryUrl(ctx, gist.User.Username, gist.Identifier()))
cmd.Env = append(cmd.Env, "OPENGIST_REPOSITORY_ID="+strconv.Itoa(int(gist.ID)))
if err = cmd.Run(); err != nil {
return errorRes(500, "Cannot run git "+serviceType+" ; "+stderr.String(), err)
}
// updatedAt is updated only if serviceType is receive-pack
if serviceType == "receive-pack" {
gist := getData(ctx, "gist").(*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.UpdatePreviewAndCount()
}
return nil
}

View 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),
})
}

View File

@@ -3,7 +3,24 @@ package web
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/utils"
"github.com/thomiceli/opengist/templates"
htmlpkg "html"
"html/template"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@@ -14,108 +31,120 @@ import (
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/public"
"github.com/thomiceli/opengist/templates"
"golang.org/x/text/language"
htmlpkg "html"
"html/template"
"io"
"net/http"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
var dev bool
var store *sessions.CookieStore
var re = regexp.MustCompile("[^a-z0-9]+")
var fm = template.FuncMap{
"split": strings.Split,
"indexByte": strings.IndexByte,
"toInt": func(i string) int {
val, _ := strconv.Atoi(i)
return val
},
"inc": func(i int) int {
return i + 1
},
"splitGit": func(i string) []string {
return strings.FieldsFunc(i, func(r rune) bool {
return r == ',' || r == ' '
})
},
"lines": func(i string) []string {
return strings.Split(i, "\n")
},
"isMarkdown": func(i string) bool {
return strings.ToLower(filepath.Ext(i)) == ".md"
},
"isCsv": func(i string) bool {
return strings.ToLower(filepath.Ext(i)) == ".csv"
},
"csvFile": func(file *git.File) *git.CsvFile {
if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
return nil
}
var (
dev bool
flashStore *sessions.CookieStore // session store for flash messages
userStore *sessions.FilesystemStore // session store for user sessions
re = regexp.MustCompile("[^a-z0-9]+")
fm = template.FuncMap{
"split": strings.Split,
"indexByte": strings.IndexByte,
"toInt": func(i string) int {
val, _ := strconv.Atoi(i)
return val
},
"inc": func(i int) int {
return i + 1
},
"splitGit": func(i string) []string {
return strings.FieldsFunc(i, func(r rune) bool {
return r == ',' || r == ' '
})
},
"lines": func(i string) []string {
return strings.Split(i, "\n")
},
"isMarkdown": func(i string) bool {
return strings.ToLower(filepath.Ext(i)) == ".md"
},
"isCsv": func(i string) bool {
return strings.ToLower(filepath.Ext(i)) == ".csv"
},
"csvFile": func(file *git.File) *git.CsvFile {
if strings.ToLower(filepath.Ext(file.Filename)) != ".csv" {
return nil
}
csvFile, err := git.ParseCsv(file)
if err != nil {
return nil
}
csvFile, err := git.ParseCsv(file)
if err != nil {
return nil
}
return csvFile
},
"httpStatusText": http.StatusText,
"loadedTime": func(startTime time.Time) string {
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
},
"slug": func(s string) string {
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
},
"avatarUrl": func(user *db.User, noGravatar bool) string {
if user.AvatarURL != "" {
return user.AvatarURL
}
return csvFile
},
"httpStatusText": http.StatusText,
"loadedTime": func(startTime time.Time) string {
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
},
"slug": func(s string) string {
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
},
"avatarUrl": func(user *db.User, noGravatar bool) string {
if user.AvatarURL != "" {
return user.AvatarURL
}
if user.MD5Hash != "" && !noGravatar {
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
}
if user.MD5Hash != "" && !noGravatar {
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
}
return defaultAvatar()
},
"asset": func(file string) string {
if dev {
return "http://localhost:16157/" + file
}
return config.C.ExternalUrl + "/" + manifestEntries[file].File
},
"dev": func() bool {
return dev
},
"defaultAvatar": defaultAvatar,
"visibilityStr": func(visibility int, lowercase bool) string {
s := "Public"
switch visibility {
case 1:
s = "Unlisted"
case 2:
s = "Private"
}
return defaultAvatar()
},
"asset": asset,
"custom": customAsset,
"dev": func() bool {
return dev
},
"defaultAvatar": defaultAvatar,
"visibilityStr": func(visibility db.Visibility, lowercase bool) string {
s := "Public"
switch visibility {
case 1:
s = "Unlisted"
case 2:
s = "Private"
}
if lowercase {
return strings.ToLower(s)
}
return s
},
"unescape": htmlpkg.UnescapeString,
"join": func(s ...string) string {
return strings.Join(s, "")
},
"toStr": func(i interface{}) string {
return fmt.Sprint(i)
},
}
if lowercase {
return strings.ToLower(s)
}
return s
},
"unescape": htmlpkg.UnescapeString,
"join": func(s ...string) string {
return strings.Join(s, "")
},
"toStr": func(i interface{}) string {
return fmt.Sprint(i)
},
"safe": func(s string) template.HTML {
return template.HTML(s)
},
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{})
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
"addMetadataToSearchQuery": addMetadataToSearchQuery,
"indexEnabled": index.Enabled,
"isUrl": func(s string) bool {
_, err := url.ParseRequestURI(s)
return err == nil
},
}
)
type Template struct {
templates *template.Template
@@ -132,8 +161,13 @@ type Server struct {
func NewServer(isDev bool) *Server {
dev = isDev
store = sessions.NewCookieStore([]byte("opengist"))
gothic.Store = store
flashStore = sessions.NewCookieStore([]byte("opengist"))
userStore = sessions.NewFilesystemStore(path.Join(config.GetHomeDir(), "sessions"),
utils.ReadKey(path.Join(config.GetHomeDir(), "sessions", "session-auth.key")),
utils.ReadKey(path.Join(config.GetHomeDir(), "sessions", "session-encrypt.key")),
)
userStore.MaxLength(10 * 1024)
gothic.Store = userStore
e := echo.New()
e.HideBanner = true
@@ -159,12 +193,25 @@ func NewServer(isDev bool) *Server {
return nil
},
}))
//e.Use(middleware.Recover())
e.Use(middleware.Recover())
e.Use(middleware.Secure())
e.Renderer = &Template{
templates: template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html")),
t := template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html"))
customPattern := filepath.Join(config.GetHomeDir(), "custom", "*.html")
matches, err := filepath.Glob(customPattern)
if err != nil {
log.Fatal().Err(err).Msg("Failed to check for custom templates")
}
if len(matches) > 0 {
t, err = t.ParseGlob(customPattern)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse custom templates")
}
}
e.Renderer = &Template{
templates: t,
}
e.HTTPErrorHandler = func(er error, ctx echo.Context) {
if err, ok := er.(*echo.HTTPError); ok {
if err.Code >= 500 {
@@ -182,11 +229,10 @@ func NewServer(isDev bool) *Server {
e.Use(sessionInit)
e.Validator = NewValidator()
e.Validator = utils.NewValidator()
if !dev {
parseManifestEntries()
e.GET("/assets/*", cacheControl(echo.WrapHandler(http.FileServer(http.FS(public.Files)))))
}
// Web based routes
@@ -204,6 +250,9 @@ func NewServer(isDev bool) *Server {
g1.GET("/", create, logged)
g1.POST("/", processCreate, logged)
g1.GET("/preview", preview, logged)
g1.GET("/healthcheck", healthcheck)
g1.GET("/register", register)
g1.POST("/register", processRegister)
@@ -218,7 +267,8 @@ func NewServer(isDev bool) *Server {
g1.DELETE("/settings/account", accountDeleteProcess, logged)
g1.POST("/settings/ssh-keys", sshKeysProcess, logged)
g1.DELETE("/settings/ssh-keys/:id", sshKeysDelete, logged)
g1.PUT("/settings/password", passwordProcess, logged)
g1.PUT("/settings/username", usernameProcess, logged)
g2 := g1.Group("/admin-panel")
{
g2.Use(adminPermission)
@@ -227,9 +277,15 @@ func NewServer(isDev bool) *Server {
g2.POST("/users/:user/delete", adminUserDelete)
g2.GET("/gists", adminGists)
g2.POST("/gists/:gist/delete", adminGistDelete)
g2.GET("/invitations", adminInvitations)
g2.POST("/invitations", adminInvitationsCreate)
g2.POST("/invitations/:id/delete", adminInvitationsDelete)
g2.POST("/sync-fs", adminSyncReposFromFS)
g2.POST("/sync-db", adminSyncReposFromDB)
g2.POST("/gc-repos", adminGcRepos)
g2.POST("/sync-previews", adminSyncGistPreviews)
g2.POST("/reset-hooks", adminResetHooks)
g2.POST("/index-gists", adminIndexGists)
g2.GET("/configuration", adminConfig)
g2.PUT("/set-config", adminSetConfig)
}
@@ -239,7 +295,13 @@ func NewServer(isDev bool) *Server {
}
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/liked", allGists, checkRequireLogin)
g1.GET("/:user/forked", allGists, checkRequireLogin)
@@ -261,9 +323,27 @@ func NewServer(isDev bool) *Server {
g3.GET("/likes", likes)
g3.POST("/fork", fork, logged)
g3.GET("/forks", forks)
g3.PUT("/checkbox", checkbox, logged, writePermission)
}
}
customFs := os.DirFS(filepath.Join(config.GetHomeDir(), "custom"))
e.GET("/assets/*", func(ctx echo.Context) error {
if _, err := public.Files.Open(path.Join("assets", ctx.Param("*"))); !dev && err == nil {
return echo.WrapHandler(http.FileServer(http.FS(public.Files)))(ctx)
}
// if the custom file is an .html template, render it
if strings.HasSuffix(ctx.Param("*"), ".html") {
if err := html(ctx, ctx.Param("*")); err != nil {
return notFound("Page not found")
}
return nil
}
return echo.WrapHandler(http.StripPrefix("/assets/", http.FileServer(http.FS(customFs))))(ctx)
})
// Git HTTP routes
if config.C.HttpGit {
e.Any("/:user/:gistname/*", gitHttp, gistSoftInit)
@@ -306,16 +386,32 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc {
setData(ctx, "c", config.C)
setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "")
setData(ctx, "gitlabOauth", config.C.GitlabClientKey != "" && config.C.GitlabSecret != "")
setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "")
setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "")
httpProtocol := "http"
if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
httpProtocol = "https"
}
setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))
var baseHttpUrl string
// if a custom external url is set, use it
if config.C.ExternalUrl != "" {
baseHttpUrl = config.C.ExternalUrl
} else {
baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
}
setData(ctx, "baseHttpUrl", baseHttpUrl)
return next(ctx)
}
}
func locale(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
// Check URL arguments
lang := ctx.Request().URL.Query().Get("lang")
changeLang := lang != ""
@@ -334,7 +430,7 @@ func locale(next echo.HandlerFunc) echo.HandlerFunc {
changeLang = false
}
//3.Then check from 'Accept-Language' header.
// 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)
@@ -393,7 +489,7 @@ func writePermission(next echo.HandlerFunc) echo.HandlerFunc {
gist := getData(ctx, "gist")
user := getUserLogged(ctx)
if !gist.(*db.Gist).CanWrite(user) {
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Uuid)
return redirect(ctx, "/"+gist.(*db.Gist).User.Username+"/"+gist.(*db.Gist).Identifier())
}
return next(ctx)
}
@@ -434,13 +530,6 @@ func checkRequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
}
}
func cacheControl(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderCacheControl, "public, max-age=31536000")
return next(c)
}
}
func noRouteFound(echo.Context) error {
return notFound("Page not found")
}
@@ -473,3 +562,18 @@ func defaultAvatar() string {
}
return config.C.ExternalUrl + "/" + manifestEntries["default.png"].File
}
func asset(file string) string {
if dev {
return "http://localhost:16157/" + file
}
return config.C.ExternalUrl + "/" + manifestEntries[file].File
}
func customAsset(file string) string {
assetpath, err := url.JoinPath("/", "assets", file)
if err != nil {
log.Error().Err(err).Msgf("Failed to join path for custom file %s", file)
}
return config.C.ExternalUrl + assetpath
}

View File

@@ -3,12 +3,18 @@ package web
import (
"crypto/md5"
"fmt"
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/db"
"golang.org/x/crypto/ssh"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/utils"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/db"
"golang.org/x/crypto/ssh"
)
func userSettings(ctx echo.Context) error {
@@ -21,6 +27,7 @@ func userSettings(ctx echo.Context) error {
setData(ctx, "email", user.Email)
setData(ctx, "sshKeys", keys)
setData(ctx, "hasPassword", user.Password != "")
setData(ctx, "htmlTitle", "Settings")
return html(ctx, "settings.html")
}
@@ -61,13 +68,13 @@ func accountDeleteProcess(ctx echo.Context) error {
func sshKeysProcess(ctx echo.Context) error {
user := getUserLogged(ctx)
var dto = new(db.SSHKeyDTO)
dto := new(db.SSHKeyDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
}
if err := ctx.Validate(dto); err != nil {
addFlash(ctx, validationMessages(&err), "error")
addFlash(ctx, utils.ValidationMessages(&err), "error")
return redirect(ctx, "/settings")
}
key := dto.ToSSHKey()
@@ -92,7 +99,6 @@ func sshKeysProcess(ctx echo.Context) error {
func sshKeysDelete(ctx echo.Context) error {
user := getUserLogged(ctx)
keyId, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
return redirect(ctx, "/settings")
}
@@ -110,3 +116,70 @@ func sshKeysDelete(ctx echo.Context) error {
addFlash(ctx, "SSH key deleted", "success")
return redirect(ctx, "/settings")
}
func passwordProcess(ctx echo.Context) error {
user := getUserLogged(ctx)
dto := new(db.UserDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, "Cannot bind data", err)
}
dto.Username = user.Username
if err := ctx.Validate(dto); err != nil {
addFlash(ctx, utils.ValidationMessages(&err), "error")
return html(ctx, "settings.html")
}
password, err := utils.Argon2id.Hash(dto.Password)
if err != nil {
return errorRes(500, "Cannot hash password", err)
}
user.Password = password
if err = user.Update(); err != nil {
return errorRes(500, "Cannot update password", err)
}
addFlash(ctx, "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, utils.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")
}
sourceDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(user.Username))
destinationDir := filepath.Join(config.GetHomeDir(), git.ReposDirectory, strings.ToLower(dto.Username))
if _, err := os.Stat(sourceDir); !os.IsNotExist(err) {
err := os.Rename(sourceDir, destinationDir)
if err != nil {
return errorRes(500, "Cannot rename user directory", err)
}
}
user.Username = dto.Username
if err := user.Update(); err != nil {
return errorRes(500, "Cannot update username", err)
}
addFlash(ctx, "Username updated", "success")
return redirect(ctx, "/settings")
}

View File

@@ -110,7 +110,7 @@ func TestVisibility(t *testing.T) {
gist1 := db.GistDTO{
Title: "gist1",
Description: "my first gist",
Private: 1,
Private: db.UnlistedVisibility,
Name: []string{""},
Content: []string{"yeah"},
}
@@ -119,25 +119,25 @@ func TestVisibility(t *testing.T) {
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
require.Equal(t, 1, gist1db.Private)
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, 2, gist1db.Private)
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, 0, gist1db.Private)
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, 1, gist1db.Private)
require.Equal(t, db.UnlistedVisibility, gist1db.Private)
}
func TestLikeFork(t *testing.T) {
@@ -198,3 +198,59 @@ func TestLikeFork(t *testing.T) {
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())
}

View File

@@ -125,7 +125,9 @@ func structToURLValues(s interface{}) url.Values {
}
func setup(t *testing.T) {
err := config.InitConfig("")
_ = os.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1")
err := config.InitConfig("", io.Discard)
require.NoError(t, err, "Could not init config")
err = os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755)
@@ -133,13 +135,15 @@ func setup(t *testing.T) {
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, "repos"), 0755)
require.NoError(t, err, "Could not create repos directory")
err = os.MkdirAll(filepath.Join(homePath, "sessions"), 0755)
require.NoError(t, err, "Could not create sessions directory")
err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
require.NoError(t, err, "Could not create tmp repos directory")
@@ -149,6 +153,9 @@ func setup(t *testing.T) {
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) {
@@ -157,6 +164,15 @@ func teardown(t *testing.T, s *testServer) {
err := db.Close()
require.NoError(t, err, "Could not close database")
err = os.RemoveAll(path.Join(config.C.OpengistHome, "tests"))
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tests"))
require.NoError(t, err, "Could not remove repos directory")
err = os.RemoveAll(path.Join(config.GetHomeDir(), "tmp", "repos"))
require.NoError(t, err, "Could not remove repos directory")
// err = os.RemoveAll(path.Join(config.C.OpengistHome, "testsindex"))
// require.NoError(t, err, "Could not remove repos directory")
// err = index.Close()
// require.NoError(t, err, "Could not close index")
}

View File

@@ -2,18 +2,12 @@ package web
import (
"context"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
"golang.org/x/crypto/argon2"
"html/template"
"net/http"
"strconv"
@@ -36,6 +30,10 @@ func getData(ctx echo.Context, key string) any {
return data[key]
}
func dataMap(ctx echo.Context) echo.Map {
return ctx.Request().Context().Value(dataKey).(echo.Map)
}
func html(ctx echo.Context, template string) error {
return htmlWithCode(ctx, 200, template)
}
@@ -70,7 +68,7 @@ func getUserLogged(ctx echo.Context) *db.User {
}
func setErrorFlashes(ctx echo.Context) {
sess, _ := store.Get(ctx.Request(), "flash")
sess, _ := flashStore.Get(ctx.Request(), "flash")
setData(ctx, "flashErrors", sess.Flashes("error"))
setData(ctx, "flashSuccess", sess.Flashes("success"))
@@ -79,13 +77,13 @@ func setErrorFlashes(ctx echo.Context) {
}
func addFlash(ctx echo.Context, flashMessage string, flashType string) {
sess, _ := store.Get(ctx.Request(), "flash")
sess, _ := flashStore.Get(ctx.Request(), "flash")
sess.AddFlash(flashMessage, flashType)
_ = sess.Save(ctx.Request(), ctx.Response())
}
func getSession(ctx echo.Context) *sessions.Session {
sess, _ := store.Get(ctx.Request(), "session")
sess, _ := userStore.Get(ctx.Request(), "session")
return sess
}
@@ -124,59 +122,6 @@ func loadSettings(ctx echo.Context) error {
return nil
}
type OpengistValidator struct {
v *validator.Validate
}
func NewValidator() *OpengistValidator {
v := validator.New()
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
return &OpengistValidator{v}
}
func (cv *OpengistValidator) Validate(i interface{}) error {
if err := cv.v.Struct(i); err != nil {
return err
}
return nil
}
func validationMessages(err *error) string {
errs := (*err).(validator.ValidationErrors)
messages := make([]string, len(errs))
for i, e := range errs {
switch e.Tag() {
case "max":
messages[i] = e.Field() + " is too long"
case "required":
messages[i] = e.Field() + " should not be empty"
case "excludes":
messages[i] = e.Field() + " should not include a sub directory"
case "alphanum":
messages[i] = e.Field() + " should only contain alphanumeric characters"
case "min":
messages[i] = "Not enough " + e.Field()
case "notreserved":
messages[i] = "Invalid " + e.Field()
}
}
return strings.Join(messages, " ; ")
}
func validateReservedKeywords(fl validator.FieldLevel) bool {
name := fl.Field().String()
restrictedNames := map[string]struct{}{}
for _, restrictedName := range []string{"assets", "register", "login", "logout", "settings", "admin-panel", "all", "search", "init"} {
restrictedNames[restrictedName] = struct{}{}
}
// if the name is not in the restricted names, it is valid
_, ok := restrictedNames[name]
return !ok
}
func getPage(ctx echo.Context) int {
page := ctx.QueryParam("page")
if page == "" {
@@ -230,67 +175,42 @@ func tr(ctx echo.Context, key string) template.HTML {
return l.Tr(key)
}
type Argon2ID struct {
format string
version int
time uint32
memory uint32
keyLen uint32
saltLen uint32
threads uint8
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
}
var argon2id = Argon2ID{
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
version: argon2.Version,
time: 1,
memory: 64 * 1024,
keyLen: 32,
saltLen: 16,
threads: 4,
}
func (a Argon2ID) hash(plain string) (string, error) {
salt := make([]byte, a.saltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}
hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)
return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}
func (a Argon2ID) verify(plain, hash string) (bool, error) {
if hash == "" {
return false, nil
}
hashParts := strings.Split(hash, "$")
if len(hashParts) != 6 {
return false, errors.New("invalid hash")
}
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
if err != nil {
return false, err
}
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
if err != nil {
return false, err
}
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
if err != nil {
return false, err
}
hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))
return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
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())
}

View File

@@ -1,71 +1,12 @@
package main
import (
"flag"
"fmt"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/memdb"
"github.com/thomiceli/opengist/internal/ssh"
"github.com/thomiceli/opengist/internal/web"
"github.com/thomiceli/opengist/internal/cli"
"os"
"path/filepath"
)
func initialize() {
fmt.Println("Opengist v" + config.OpengistVersion)
configPath := flag.String("config", "", "Path to a config file in YML format")
flag.Parse()
if err := config.InitConfig(*configPath); err != nil {
panic(err)
}
if err := os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755); err != nil {
panic(err)
}
config.InitLog()
gitVersion, err := git.GetGitVersion()
if err != nil {
log.Fatal().Err(err).Send()
}
if ok, err := config.CheckGitVersion(gitVersion); err != nil {
log.Fatal().Err(err).Send()
} else if !ok {
log.Warn().Msg("Git version may be too old, as Opengist has not been tested prior git version 2.20. " +
"Current git version: " + gitVersion)
}
homePath := config.GetHomeDir()
log.Info().Msg("Data directory: " + homePath)
if err := os.MkdirAll(filepath.Join(homePath, "repos"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
if err := os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755); err != nil {
log.Fatal().Err(err).Send()
}
log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename))
if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), false); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize database")
}
if err := memdb.Setup(); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize in memory database")
}
}
func main() {
initialize()
go web.NewServer(os.Getenv("OG_DEV") == "1").Start()
go ssh.Start()
select {}
if err := cli.App(); err != nil {
os.Exit(1)
}
}

6157
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,10 @@
{
"name": "opengist",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "node_modules/.bin/vite",
"build": "node_modules/.bin/vite build",
"preview": "node_modules/.bin/vite preview"
"dev": "node_modules/.bin/vite -c public/vite.config.js",
"build": "node_modules/.bin/vite -c public/vite.config.js build",
"preview": "node_modules/.bin/vite -c public/vite.config.js preview"
},
"devDependencies": {
"@codemirror/commands": "^6.2.2",
@@ -20,17 +19,17 @@
"codemirror": "^6.0.1",
"cssnano": "^5.1.15",
"dayjs": "^1.11.9",
"github-markdown-css": "^5.2.0",
"highlight.js": "^11.7.0",
"markdown-it": "^13.0.1",
"github-markdown-css": "^5.5.0",
"nodemon": "^2.0.22",
"postcss": "^8.4.13",
"postcss": "^8.4.32",
"postcss-cli": "^11.0.0",
"postcss-cssnext": "^3.1.1",
"postcss-import": "^15.1.0",
"postcss-loader": "^7.1.0",
"postcss-selector-namespace": "^3.0.1",
"sass": "^1.62.1",
"sugarss": "^4.0.1",
"tailwindcss": "^3.2.7",
"vite": "^4.2.3"
"vite": "^4.5.2"
}
}

7
postcss.config.js vendored
View File

@@ -1,7 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
cssnano: {},
},
}

View File

@@ -5,16 +5,27 @@ document.addEventListener('DOMContentLoaded', () => {
registerDomSetting(elem as HTMLElement)
})
}
let copyInviteButtons = Array.from(document.getElementsByClassName("copy-invitation-link"));
for (let button of copyInviteButtons) {
button.addEventListener('click', () => {
navigator.clipboard.writeText((button as HTMLElement).dataset.link).catch((err) => {
console.error('Could not copy text: ', err);
});
})
}
});
const setSetting = (key: string, value: string) => {
// @ts-ignore
const baseUrl = window.opengist_base_url || '';
const data = new URLSearchParams();
data.append('key', key);
data.append('value', value);
if (document.getElementsByName('_csrf').length !== 0) {
data.append('_csrf', ((document.getElementsByName('_csrf')[0] as HTMLInputElement).value));
}
return fetch('/admin-panel/set-config', {
return fetch(`${baseUrl}/admin-panel/set-config`, {
method: 'PUT',
credentials: 'same-origin',
body: data,

74
public/catppuccin-latte.css vendored Normal file
View 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
View 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 }

View File

@@ -34,6 +34,45 @@ document.addEventListener("DOMContentLoaded", () => {
],
});
let mdpreview = dom.querySelector(".md-preview") as HTMLElement;
// event if the filename ends with .md; trigger event
dom.querySelector<HTMLInputElement>(".form-filename")!.onkeyup = (e) => {
let filename = (e.target as HTMLInputElement).value;
if (filename.endsWith(".md")) {
mdpreview!.classList.remove("hidden");
} else {
mdpreview!.classList.add("hidden");
}
};
// @ts-ignore
const baseUrl = window.opengist_base_url || '';
let previewShown = false;
mdpreview.onclick = () => {
previewShown = !previewShown;
let divpreview = dom.querySelector("div.preview") as HTMLElement;
let cmeditor = dom.querySelector(".cm-editor") as HTMLElement;
if (!previewShown) {
divpreview!.classList.add("hidden");
cmeditor!.classList.remove("hidden-important");
return;
} else {
fetch(`${baseUrl}/preview?` + new URLSearchParams({
content: editor.state.doc.toString()
}), {
method: 'GET',
credentials: 'same-origin',
}).then(r => r.text()).then(r => {
let divpreview = dom.querySelector("div.preview") as HTMLElement;
divpreview!.innerHTML = r;
divpreview!.classList.remove("hidden");
cmeditor!.classList.add("hidden-important");
})
}
}
dom.querySelector<HTMLInputElement>(".editor-indent-type")!.onchange = (e) => {
let newTabType = (e.target as HTMLInputElement).value;
setIndentType(editor, !["tab", "space"].includes(newTabType) ? "space" : newTabType);
@@ -165,6 +204,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 = () => {
window.onbeforeunload = null;
};

112
public/embed.scss vendored Normal file
View 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
View File

@@ -0,0 +1 @@
import "./embed.scss"

76
public/gist.ts Normal file
View 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;
});
}

View File

@@ -1,49 +0,0 @@
import hljs from 'highlight.js';
import md from 'markdown-it';
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.getLanguage(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;
}
});
});

View File

@@ -1,4 +1,3 @@
import './style.css';
import './style.scss';
import './favicon-32.png';
import './opengist.svg';
@@ -54,6 +53,22 @@ document.addEventListener('DOMContentLoaded', () => {
e.innerHTML = dayjs.unix(parseInt(e.innerHTML)).format('DD/MM/YYYY HH:mm');
});
document.querySelectorAll('form').forEach((form: HTMLFormElement) => {
form.onsubmit = () => {
form.querySelectorAll('input[type=datetime-local]').forEach((input: HTMLInputElement) => {
console.log(dayjs(input.value).unix());
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'expiredAtUnix'
hiddenInput.value = dayjs(input.value).unix().toString();
form.appendChild(hiddenInput);
});
return true;
};
})
const rev = document.querySelector<HTMLElement>('.revision-text');
if (rev) {
const fullRev = rev.innerHTML;
@@ -154,12 +169,27 @@ document.addEventListener('DOMContentLoaded', () => {
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 = (el as HTMLElement).dataset.visibility || '0';
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');
})
});

12
public/postcss.config.js vendored Normal file
View 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
View File

@@ -2,6 +2,8 @@
@tailwind components;
@tailwind utilities;
@config "./tailwind.config.js";
@layer base {
ul, ol {
list-style: revert;
@@ -98,10 +100,6 @@ pre {
max-height: 337px;
}
.hljs {
color: #c9d1d9;
}
.line-code.selected {
background-color: rgb(255, 247, 190) !important;
box-shadow: inset 4px 0 0 rgb(255, 213, 65) !important;
@@ -156,3 +154,27 @@ dl.dl-config dd {
.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;
}
.hidden-important {
@apply hidden !important;
}

6
public/style.scss vendored
View File

@@ -1,9 +1,11 @@
:root {
@import "github-markdown-css/github-markdown-light";
@import 'highlight.js/scss/base16/one-light.scss';
@import './catppuccin-latte';
}
.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
View 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',
}

View File

@@ -1,5 +1,6 @@
const colors = require('tailwindcss/colors')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./templates/**/*.html",

12
public/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"esModuleInterop": true
},
"files": [
"main.ts",
"editor.ts",
"admin.ts",
"gist.ts",
"embed.ts",
],
}

View File

@@ -9,7 +9,13 @@ export default defineConfig({
assetsDir: 'assets',
manifest: true,
rollupOptions: {
input: ['./public/main.ts', './public/editor.ts', './public/admin.ts', './public/hljs.ts']
input: [
'./public/main.ts',
'./public/editor.ts',
'./public/admin.ts',
'./public/gist.ts',
'./public/embed.ts'
]
},
assetsInlineLimit: 0,
}

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
set -euo pipefail
make watch_frontend &

View File

@@ -5,9 +5,10 @@
{{ define "admin_footer" }}
{{ if .urlPage }}
<div class="flex mt-4 justify-center space-x-2">
{{ template "pagination" . }}
{{ template "_pagination" . }}
</div>
{{ end }}
<script src="{{ asset "admin.ts" }}"></script>
</main>
</div>
{{ end }}

View File

@@ -15,6 +15,8 @@
{{ 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="{{ $.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">{{ .locale.Tr "admin.gists" }}</a>
<a href="{{ $.c.ExternalUrl }}/admin-panel/invitations" class="{{ if eq .adminHeaderPage "invitations" }}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">{{ .locale.Tr "admin.invitations" }}</a>
<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">{{ .locale.Tr "admin.configuration" }}</a>
</nav>

View File

@@ -5,7 +5,7 @@
{{ define "footer" }}
<div class="inline-flex py-8">
<p class="text-slate-600 dark:text-slate-400 [&>*]:mx-1.5 flex">
<p class="text-slate-600 dark:text-slate-400 [&>*]:mx-1.5 -ml-1.5 flex">
<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">
<span class="mr-1">{{ .locale.Tr "footer.powered-by" "<span class=\"font-bold dark:text-slate-300\">Opengist</span>" }} </span>
@@ -23,16 +23,20 @@
<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-full hover:text-slate-500 dark:hover:text-slate-400" role="menuitem" tabindex="-1" id="menu-item-0">{{ .Name }}</a>
<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>
{{ if ne (len .c.StaticLinks) 0 }}
<div class="ml-1.5">
{{ range $index, $value := .c.StaticLinks }}
<a href="{{ if isUrl .Path }}{{ .Path }}{{ else }}{{ $.c.ExternalUrl }}/assets/{{ .Path }}{{ end }}" class="text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 inline-flex">{{ .Name }}</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
<script type="module" src="{{ asset "hljs.ts" }}"></script>
</div>
</body>
</html>

View File

@@ -7,7 +7,10 @@
<meta name="robots" content="noindex, follow">
{{ end }}
<base href="{{ $.c.ExternalUrl }}" />
<script>
window.opengist_base_url = "{{ $.c.ExternalUrl }}";
const checkTheme = () => {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
@@ -25,12 +28,16 @@
)
</script>
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset "favicon-32.png" }}">
{{ if $.c.CustomFavicon }}
<link rel="icon" type="image/png" sizes="32x32" href="{{ custom $.c.CustomFavicon }}">
{{ else }}
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset "favicon-32.png" }}">
{{ end }}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{ if dev }}
<script type="module" src="{{ asset "@vite/client" }}"></script>
<link rel="stylesheet" href="{{ asset "style.css" }}" />
<link rel="stylesheet" href="{{ asset "style.scss" }}" />
<script type="module" src="{{ asset "main.ts" }}"></script>
{{ else }}
<link rel="stylesheet" href="{{ asset "main.css" }}" />
@@ -63,13 +70,21 @@
</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">
{{ if $.c.CustomLogo }}
<img src="{{ custom $.c.CustomLogo }}" class="object-cover h-12">
{{ else }}
<img src="{{ asset "opengist.svg" }}" class="object-cover h-12 w-12">
{{ end }}
</a>
</div>
<div class="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
<div class="flex-shrink-0 items-center flex sm:hidden">
<a href="{{ $.c.ExternalUrl }}/">
<img src="{{ asset "opengist.svg" }}" class="object-cover h-12 w-12">
{{ if $.c.CustomLogo }}
<img src="{{ custom $.c.CustomLogo }}" class="object-cover h-12">
{{ else }}
<img src="{{ asset "opengist.svg" }}" class="object-cover h-12 w-12">
{{ end }}
</a>
</div>
<div class="hidden sm:block sm:ml-6">
@@ -85,10 +100,23 @@
<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>
</div>
<form action="/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 }}">
<form action="{{ $.c.ExternalUrl }}/search" method="GET">
<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">
</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>
@@ -105,15 +133,15 @@
</svg>
</div>
<div class="hidden relative sm:inline-block text-left">
<div id="user-menu" class="hidden w-56 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">
<a href="{{ $.c.ExternalUrl }}/{{ .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">
<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>
{{ .locale.Tr "header.menu.my-gists" }}
</a>
<a href="{{ $.c.ExternalUrl }}/{{ .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">
<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>
@@ -122,7 +150,7 @@
</div>
{{ if .userLogged.IsAdmin }}
<div class="py-1" role="none">
<a href="{{ $.c.ExternalUrl }}/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">
<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>
@@ -131,14 +159,14 @@
</div>
{{ end }}
<div class="py-1" role="none">
<a href="{{ $.c.ExternalUrl }}/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">
<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" />
</svg>
{{ .locale.Tr "header.menu.settings" }}
</a>
<a href="{{ $.c.ExternalUrl }}/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">
<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>
@@ -161,7 +189,7 @@
{{ end }}
<div class="hidden sm:block ml-2 border-l-1 border-gray-200 dark:border-gray-600 rounded-md"><br /></div>
<div id="theme-btn" class="hidden sm:flex items-center ml-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md px-3 py-2">
<div id="theme-btn" class="sm:flex items-center ml-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md px-3 py-2">
<div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-primary-500 dark:hidden">
<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" />
@@ -170,7 +198,7 @@
<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>
</div>
<div class="hidden relative sm:inline-block text-left">
<div class="relative sm:inline-block text-left">
<div id="theme-menu" class="hidden 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">
<!-- Active: "bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900", Not Active: "text-gray-300 dark:text-gray-700" -->
@@ -186,7 +214,7 @@
</svg>
{{ .locale.Tr "header.menu.dark" }}
</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">
<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>
@@ -211,7 +239,7 @@
<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>
</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 type="submit" hidden="hidden">
</form>

View File

@@ -4,6 +4,7 @@
{{ end }}
{{ define "gist_footer" }}
</main>
</div>
{{ end }}

Some files were not shown because too many files have changed in this diff Show More