Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
58
.drone.yml
58
.drone.yml
|
@ -1,58 +0,0 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: lint and test
|
||||
|
||||
steps:
|
||||
- name: lint
|
||||
image: code.pztrn.name/containers/mirror/golangci/golangci-lint:v1.48.0
|
||||
pull: if-not-exists
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
commands:
|
||||
- golangci-lint run
|
||||
|
||||
- name: test
|
||||
image: code.pztrn.name/containers/mirror/golang:1.19.0-alpine
|
||||
pull: if-not-exists
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
commands:
|
||||
- go test ./...
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build docker images
|
||||
|
||||
depends_on:
|
||||
- "lint and test"
|
||||
|
||||
steps:
|
||||
- name: build master image
|
||||
image: code.pztrn.name/containers/mirror/plugins/docker:20.13.0
|
||||
pull: if-not-exists
|
||||
privileged: true
|
||||
when:
|
||||
branch: ["master"]
|
||||
settings:
|
||||
registry: code.pztrn.name
|
||||
username: drone
|
||||
password:
|
||||
from_secret: drone_secret
|
||||
repo: code.pztrn.name/apps/fastpastebin
|
||||
auto_tag: true
|
||||
|
||||
- name: build tagged image
|
||||
image: code.pztrn.name/containers/mirror/plugins/docker:20.13.0
|
||||
pull: if-not-exists
|
||||
privileged: true
|
||||
when:
|
||||
event: ["tag"]
|
||||
settings:
|
||||
registry: code.pztrn.name
|
||||
username: drone
|
||||
password:
|
||||
from_secret: drone_secret
|
||||
repo: code.pztrn.name/apps/fastpastebin
|
||||
auto_tag: true
|
|
@ -1,7 +1 @@
|
|||
examples/fastpastebin.yaml
|
||||
dist/
|
||||
data/
|
||||
vendor/
|
||||
.idea
|
||||
.vscode
|
||||
*DS_Store*
|
||||
examples/fastpastebin.yaml
|
|
@ -1,38 +0,0 @@
|
|||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375/
|
||||
DOCKER_DRIVER: overlay2
|
||||
DOCKER_TCP_PORT: 2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
CONTAINER_NAME: registry.gitlab.pztrn.name/fastpastebin/fastpastebin
|
||||
DIND_IMAGE: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:dind
|
||||
GOLANGCILINT_IMAGE: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.40.1-alpine
|
||||
|
||||
services:
|
||||
- name: ${DIND_IMAGE}
|
||||
alias: docker
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
image: ${GOLANGCILINT_IMAGE}
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- golangci-lint run ./...
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: $DIND_IMAGE
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- source docker/set_docker_tag.sh
|
||||
- docker build -t $CONTAINER_NAME:$DOCKER_TAG .
|
||||
- docker push $CONTAINER_NAME:$DOCKER_TAG
|
||||
only:
|
||||
- tags
|
||||
- master
|
|
@ -1,35 +0,0 @@
|
|||
run:
|
||||
deadline: 5m
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
# Because globals might exist, but according to our codestyle they
|
||||
# should be lowercased and considered as unexported.
|
||||
- gochecknoglobals
|
||||
# While it might be useful it'll create more problems that will solve.
|
||||
- gocritic
|
||||
# Complains about main() lengths, which isn't an issue.
|
||||
- funlen
|
||||
# Magic numbers everywhere and we can't get rid of them.
|
||||
- gomnd
|
||||
# This linter MIGHT BE good, but who decided that I want keepFor in
|
||||
# JSON instead of keep_for for KeepFor field?
|
||||
- tagliatelle
|
||||
# Deprecated.
|
||||
- exhaustivestruct
|
||||
linters-settings:
|
||||
lll:
|
||||
line-length: 420
|
||||
gocognit:
|
||||
min-complexity: 50
|
||||
gocyclo:
|
||||
min-complexity: 40
|
||||
cyclop:
|
||||
max-complexity: 40
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# There will be some ToDos.
|
||||
- linters:
|
||||
- godox
|
||||
text: "TODO"
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"line-length": false,
|
||||
"first-line-h1": false,
|
||||
"no-duplicate-header": false
|
||||
}
|
84
CHANGELOG.md
84
CHANGELOG.md
|
@ -1,84 +1,12 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
``[A]`` - added
|
||||
``[F]`` - fixed
|
||||
``[R]`` - removed
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.1] - 2022-08-14
|
||||
|
||||
### Changed
|
||||
|
||||
* Update docker images - alpine to 3.16.1, golang to 1.19, golangci-lint to 1.48.0.
|
||||
* Update chroma to v2.2.0.
|
||||
* Update bulma to 0.9.4.
|
||||
* Update bulma-tooltip to 1.2 (was 3.0.0, but repo was switched to CreativeBulma).
|
||||
* Update github.com/dchest/captcha to v1.0.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Put valid repository's link in footer.
|
||||
|
||||
### Removed
|
||||
|
||||
* Removed `flagger` dependency.
|
||||
|
||||
## [0.4.0] - 2021-01-09
|
||||
|
||||
### Added
|
||||
|
||||
* PostgreSQL support.
|
||||
* Docker containerization for every commit and tag.
|
||||
* Pastes cleanup procedure.
|
||||
|
||||
### Changed
|
||||
|
||||
* Updated bulma to v 0.7.5.
|
||||
* Moved from `io/ioutil` to `os` package for reading/writing files and directories.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Dirty hack to get database connection reestablish (for sure).
|
||||
|
||||
## 0.3.0
|
||||
|
||||
Release changelogs lost :(.
|
||||
|
||||
## [0.2.0] - 2018-05-27
|
||||
|
||||
### Added
|
||||
|
||||
* Possibility to create different database backends. Currently `mysql` and `flatfiles` are available.
|
||||
|
||||
### Changed
|
||||
|
||||
* De-hardcoded pagination configuration, it is now configurable via configuration file.
|
||||
|
||||
## [0.1.1] - 2018-05-26
|
||||
|
||||
### Added
|
||||
|
||||
* Footer copyrights.
|
||||
|
||||
### Changed
|
||||
|
||||
* Refactored templates: now they're included in each other if neccessary.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed nasty bugs with private pastes that causing fastpastebin to crash.
|
||||
* Logger level from configuration now properly set.
|
||||
|
||||
## [0.1.0] - 2018-05-19
|
||||
## 0.1.0
|
||||
|
||||
First normal release. Fast Paste Bin is able to handle public, private
|
||||
and passworded pastes.
|
||||
|
||||
[Unreleased]: https://code.pztrn.name/apps/fastpastebin/compare/v0.4.1...HEAD
|
||||
[0.4.1]: https://code.pztrn.name/apps/fastpastebin/compare/0.4.0...v0.4.1
|
||||
[0.4.0]: https://code.pztrn.name/apps/fastpastebin/compare/v0.2.0...0.4.0
|
||||
[0.2.0]: https://code.pztrn.name/apps/fastpastebin/compare/v0.1.1...v0.2.0
|
||||
[0.1.1]: https://code.pztrn.name/apps/fastpastebin/compare/v0.1.0...v0.1.1
|
||||
[0.1.0]: https://code.pztrn.name/apps/fastpastebin/src/tag/v0.1.0
|
||||
and passworded pastes.
|
17
Dockerfile
17
Dockerfile
|
@ -1,17 +0,0 @@
|
|||
FROM code.pztrn.name/containers/mirror/golang:1.19.0-alpine AS build
|
||||
|
||||
WORKDIR /fastpastebin
|
||||
COPY . .
|
||||
|
||||
WORKDIR /fastpastebin/cmd/fastpastebin
|
||||
|
||||
RUN CGO_ENABLED=0 go build -tags netgo
|
||||
|
||||
FROM code.pztrn.name/containers/mirror/alpine:3.16.1
|
||||
LABEL maintainer "Stanislav N. <pztrn@pztrn.name>"
|
||||
|
||||
COPY --from=build /fastpastebin/cmd/fastpastebin/fastpastebin /app/fastpastebin
|
||||
COPY docker/fastpastebin.docker.yaml /app/fastpastebin.yaml
|
||||
|
||||
EXPOSE 25544
|
||||
ENTRYPOINT [ "/app/fastpastebin", "-config", "/app/fastpastebin.yaml" ]
|
|
@ -0,0 +1,187 @@
|
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/alecthomas/chroma"
|
||||
packages = [
|
||||
".",
|
||||
"formatters",
|
||||
"formatters/html",
|
||||
"lexers",
|
||||
"lexers/a",
|
||||
"lexers/b",
|
||||
"lexers/c",
|
||||
"lexers/d",
|
||||
"lexers/e",
|
||||
"lexers/f",
|
||||
"lexers/g",
|
||||
"lexers/h",
|
||||
"lexers/i",
|
||||
"lexers/internal",
|
||||
"lexers/j",
|
||||
"lexers/k",
|
||||
"lexers/l",
|
||||
"lexers/m",
|
||||
"lexers/n",
|
||||
"lexers/o",
|
||||
"lexers/p",
|
||||
"lexers/q",
|
||||
"lexers/r",
|
||||
"lexers/s",
|
||||
"lexers/t",
|
||||
"lexers/v",
|
||||
"lexers/w",
|
||||
"lexers/x",
|
||||
"lexers/y",
|
||||
"styles"
|
||||
]
|
||||
revision = "3020e2ea8c6b1a9c2336022d847c4392c3997f02"
|
||||
version = "v0.4.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/danwakefield/fnmatch"
|
||||
packages = ["."]
|
||||
revision = "cbb64ac3d964b81592e64f957ad53df015803288"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dchest/captcha"
|
||||
packages = ["."]
|
||||
revision = "6a29415a8364ec2971fdc62d9e415ed53fc20410"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dlclark/regexp2"
|
||||
packages = [
|
||||
".",
|
||||
"syntax"
|
||||
]
|
||||
revision = "487489b64fb796de2e55f4e8a4ad1e145f80e957"
|
||||
version = "v1.1.6"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-sql-driver/mysql"
|
||||
packages = ["."]
|
||||
revision = "a0583e0143b1624142adab07e0e97fe106d99561"
|
||||
version = "v1.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jmoiron/sqlx"
|
||||
packages = [
|
||||
".",
|
||||
"reflectx"
|
||||
]
|
||||
revision = "2aeb6a910c2b94f2d5eb53d9895d80e27264ec41"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/labstack/echo"
|
||||
packages = [
|
||||
".",
|
||||
"middleware"
|
||||
]
|
||||
revision = "6d227dfea4d2e52cb76856120b3c17f758139b4e"
|
||||
version = "3.3.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/labstack/gommon"
|
||||
packages = [
|
||||
"bytes",
|
||||
"color",
|
||||
"log",
|
||||
"random"
|
||||
]
|
||||
revision = "588f4e8bddc6cb45c27b448e925c7fd6a5545434"
|
||||
version = "0.2.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pressly/goose"
|
||||
packages = ["."]
|
||||
revision = "056a4d47dcc4d67fa3947a4f13945a5c690e568b"
|
||||
version = "v2.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/pztrn/flagger"
|
||||
packages = ["."]
|
||||
revision = "1330c5f1b64f253b0505ee4a2417fb8be856b87d"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/rs/zerolog"
|
||||
packages = [
|
||||
".",
|
||||
"internal/cbor",
|
||||
"internal/json"
|
||||
]
|
||||
revision = "05eafee0eb17d0150591a8f30f0fa592cc9b7471"
|
||||
version = "v1.6.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/valyala/bytebufferpool"
|
||||
packages = ["."]
|
||||
revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/valyala/fasttemplate"
|
||||
packages = ["."]
|
||||
revision = "dcecefd839c4193db0d35b88ec65b4c12d360ab0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"acme",
|
||||
"acme/autocert",
|
||||
"pbkdf2",
|
||||
"scrypt"
|
||||
]
|
||||
revision = "b49d69b5da943f7ef3c9cf91c8777c1f78a0cc3c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"webdav",
|
||||
"webdav/internal/xml"
|
||||
]
|
||||
revision = "5f9ae10d9af5b1c89ae6904293b14b064d4ada23"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
revision = "cbbc999da32df943dac6cd71eb3ee39e1d7838b9"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "aea0cd48405b88f2c799a3d994b952758f29e06ada92b4bbe6cc4ff105d95d59"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
|
@ -0,0 +1,34 @@
|
|||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/rs/zerolog"
|
||||
version = "1.6.0"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
102
README.md
102
README.md
|
@ -1,90 +1,80 @@
|
|||
[Chat on Keybase.io](https://keybase.io/team/fastpastebin)
|
||||
|
||||
# Fast Pastebin
|
||||
|
||||
[![Build Status](https://ci.code.pztrn.name/api/badges/apps/fastpastebin/status.svg)](https://ci.code.pztrn.name/apps/fastpastebin)
|
||||
Easy-to-use-and-install pastebin software written in Go. No bells or
|
||||
whistles, no websockets and even NO JAVASCRIPT!(*)
|
||||
|
||||
Easy-to-use-and-install pastebin software written in Go. No bells or whistles, no websockets and even NO JAVASCRIPT!
|
||||
(*) Except fontawesome, because it's awesome :).
|
||||
|
||||
**Please, use [my gitea](https://code.pztrn.name/apps/fastpastebin) for bug reporting. All other places are mirrors!**
|
||||
|
||||
Also, [join Matrix room](https://matrix.to/#/%23fastpastebin:pztrn.online?via=matrix.org) for near-realtime chat.
|
||||
|
||||
## Current functionality
|
||||
# Current functionality.
|
||||
|
||||
* Create and view public and private pastes.
|
||||
* Syntax highlighting.
|
||||
* Pastes expiration.
|
||||
* Passwords for pastes.
|
||||
* Multiple storage backends. Currently: ``flatfiles``, ``mysql`` and ``postgresql``.
|
||||
|
||||
## Caveats
|
||||
# Caveats.
|
||||
|
||||
* Not known at this moment.
|
||||
* No links at lines numbers. See https://github.com/alecthomas/chroma/issues/132
|
||||
|
||||
## Installation and updating
|
||||
# Installation and updating
|
||||
|
||||
Just issue:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=0 go install go.dev.pztrn.name/fastpastebin/cmd/fastpastebin@VERSION
|
||||
```
|
||||
|
||||
Replace `VERSION` with a [tag of your choice](https://code.pztrn.name/apps/fastpastebin/releases).
|
||||
go get -u -v github.com/pztrn/fastpastebin/cmd/fastpastebin
|
||||
```
|
||||
|
||||
This command can be used to update Fast Paste Bin.
|
||||
|
||||
Also Fast Paste Bin is dockerized, see [here](https://code.pztrn.name/apps/-/packages/container/fastpastebin) for instructions.
|
||||
**WARNING:** installation by compiling Fast Paste Bin from sources **require**
|
||||
at least 300 megabytes of free RAM! Eventually it'll run even on 64MB-powered
|
||||
VM, it's only a compilation issue.
|
||||
|
||||
Compose file with resources limits, as used by me:
|
||||
# Configuration.
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "2.4"
|
||||
Take a look at [example configuration file](examples/fastpastebin.yaml.dist)
|
||||
which contains all supported options and their descriptions.
|
||||
|
||||
services:
|
||||
fastpastebin:
|
||||
restart: always
|
||||
image: code.pztrn.name/apps/fastpastebin:0.4.1
|
||||
volumes:
|
||||
- "./fastpastebin.yaml:/app/fastpastebin.yaml"
|
||||
ports:
|
||||
- "25544:25544"
|
||||
cpus: 2
|
||||
mem_limit: 1G
|
||||
memswap_limit: 0
|
||||
Configuration file position is irrelevant, there is no hardcoded paths where
|
||||
Fast Paste Bin looking for it's configuration. Use ``-config`` CLI parameter
|
||||
or ``FASTPASTEBIN_CONFIG`` environment variable to specify path.
|
||||
|
||||
# Developing
|
||||
|
||||
Developers should install https://github.com/UnnoTed/fileb0x/ which is used
|
||||
as replacement to go-bindata for embedding assets into binary. After changing
|
||||
assets they should be recompiled into Go code. At repository root execute
|
||||
this command and you'll be fine:
|
||||
|
||||
```
|
||||
fileb0x fileb0x.yml
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Also if you're changed list of assets (by creating or deleting them) be sure
|
||||
to fix files list in ``fileb0x.yml`` file!
|
||||
|
||||
Take a look at [example configuration file](examples/fastpastebin.yaml.dist) which contains all supported options and their descriptions.
|
||||
The rest is default - use linters, formatters, etc. VSCode with Go plugin is
|
||||
recommended for developing as it will perform most of linting-formatting
|
||||
actions automagically. Try to follow https://github.com/golang/go/wiki/CodeReviewComments
|
||||
with few exceptions:
|
||||
|
||||
Configuration file position is irrelevant, there is no hardcoded paths where Fast Paste Bin looking for it's configuration. Use ``-config`` CLI parameter or ``FASTPASTEBIN_CONFIG`` environment variable to specify path.
|
||||
* Imports should be organized in 3 groups: stdlib, local, other. See
|
||||
https://github.com/pztrn/fastpastebin/blob/master/pastes/api_http.go for
|
||||
example.
|
||||
|
||||
## Developing
|
||||
* We're not forcing any limits on line length for code, only for comments,
|
||||
they should be 72-76 chars long.
|
||||
|
||||
### Branching, versions, etc
|
||||
# ToDo
|
||||
|
||||
There is a `develop` branch which represents current development state. **All new commits (by me) and merge requests (by others) should go to that branch**.
|
||||
|
||||
Branch `master` represents "latest version" state and always stable.
|
||||
|
||||
### Code
|
||||
|
||||
Use linters, formatters, etc. VSCode with Go plugin is recommended for developing as it will perform most of linting-formatting
|
||||
actions automagically.
|
||||
|
||||
Also, Sublime Text with LSP-gopls will also work just fine.
|
||||
|
||||
Try to follow [Go's code review comments](https://github.com/golang/go/wiki/CodeReviewComments) with few exceptions:
|
||||
|
||||
* We're not forcing any limits on line length for code, only for comments, they should be 72-76 chars long.
|
||||
|
||||
## ToDo
|
||||
|
||||
This is a ToDo list which isn't sorted by any parameter at all. Just a list of tasks you can help with.
|
||||
This is a ToDo list which isn't sorted by any parameter at all. Just a list
|
||||
of tasks you can help with.
|
||||
|
||||
* User CP.
|
||||
* Files uploading.
|
||||
* Passwords for files.
|
||||
* Pastes forking and revisioning (like git or github gists).
|
||||
* Possibility to copy-paste-edit WYSIWYG content.
|
||||
* CLI client for pastes and files uploading.
|
||||
* Possibility to copy-paste-edit WISYWIG content.
|
||||
* CLI client for pastes and files uploading.
|
|
@ -22,24 +22,32 @@
|
|||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package dbnotavailable
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
// other
|
||||
"github.com/labstack/echo"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/templater"
|
||||
)
|
||||
|
||||
// Database not available error page.
|
||||
func dbNotAvailableGet(ec echo.Context) error {
|
||||
htmlData := templater.GetTemplate(ec, "database_not_available.html", nil)
|
||||
// Logs Echo requests.
|
||||
func echoReqLog(ec echo.Context, next echo.HandlerFunc) error {
|
||||
c.Logger.Info().
|
||||
Str("IP", ec.RealIP()).
|
||||
Str("Host", ec.Request().Host).
|
||||
Str("Method", ec.Request().Method).
|
||||
Str("Path", ec.Request().URL.Path).
|
||||
Str("UA", ec.Request().UserAgent()).
|
||||
Msg("HTTP request")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ec.HTML(http.StatusInternalServerError, htmlData)
|
||||
next(ec)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbNotAvailableRawGet(ec echo.Context) error {
|
||||
//nolint:wrapcheck
|
||||
return ec.String(http.StatusInternalServerError, "Database not available\nSomething went wrong while trying to connect to database. Check logs for details.")
|
||||
// Wrapper around previous function.
|
||||
func echoReqLogger() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return echoReqLog(c, next)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,22 +22,49 @@
|
|||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package postgresql
|
||||
package api
|
||||
|
||||
import (
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/api/http"
|
||||
"github.com/pztrn/fastpastebin/api/json"
|
||||
"github.com/pztrn/fastpastebin/context"
|
||||
|
||||
// other
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *context.Context
|
||||
dbAdapter *Database
|
||||
c *context.Context
|
||||
e *echo.Echo
|
||||
)
|
||||
|
||||
// New initializes variables for api package.
|
||||
func New(cc *context.Context) {
|
||||
ctx = cc
|
||||
//nolint:exhaustruct
|
||||
dbAdapter = &Database{}
|
||||
|
||||
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
|
||||
c = cc
|
||||
}
|
||||
|
||||
// InitializeAPI initializes HTTP API and starts web server.
|
||||
func InitializeAPI() {
|
||||
c.Logger.Info().Msg("Initializing HTTP server...")
|
||||
|
||||
e = echo.New()
|
||||
e.Use(echoReqLogger())
|
||||
e.Use(middleware.Recover())
|
||||
e.DisableHTTP2 = true
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
|
||||
c.RegisterEcho(e)
|
||||
|
||||
http.New(c)
|
||||
json.New(c)
|
||||
|
||||
listenAddress := c.Config.HTTP.Address + ":" + c.Config.HTTP.Port
|
||||
|
||||
go func() {
|
||||
e.Logger.Fatal(e.Start(listenAddress))
|
||||
}()
|
||||
c.Logger.Info().Msgf("Started HTTP server at %s", listenAddress)
|
||||
}
|
|
@ -22,24 +22,30 @@
|
|||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package flatfiles
|
||||
package http
|
||||
|
||||
import (
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
|
||||
)
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/api/http/static"
|
||||
"github.com/pztrn/fastpastebin/context"
|
||||
|
||||
const FlatFileDialect = "flatfiles"
|
||||
// other
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *context.Context
|
||||
flf *FlatFiles
|
||||
c *context.Context
|
||||
)
|
||||
|
||||
// New initializes basic HTTP API, which shows only index page and serves
|
||||
// static files.
|
||||
func New(cc *context.Context) {
|
||||
ctx = cc
|
||||
//nolint:exhaustruct
|
||||
flf = &FlatFiles{}
|
||||
c = cc
|
||||
c.Logger.Info().Msg("Initializing HTTP API...")
|
||||
|
||||
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
|
||||
// Static files.
|
||||
c.Echo.GET("/static/*", echo.WrapHandler(static.Handler))
|
||||
|
||||
// Index.
|
||||
c.Echo.GET("/", indexGet)
|
||||
}
|
|
@ -22,40 +22,42 @@
|
|||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package indexpage
|
||||
package http
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/api/http/static"
|
||||
"github.com/pztrn/fastpastebin/captcha"
|
||||
|
||||
// other
|
||||
"github.com/alecthomas/chroma/lexers"
|
||||
"github.com/labstack/echo"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/captcha"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/flatfiles"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/templater"
|
||||
)
|
||||
|
||||
// Index of this site.
|
||||
func indexGet(ectx echo.Context) error {
|
||||
// We should check if database connection available.
|
||||
dbConn := ctx.Database.GetDatabaseConnection()
|
||||
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusFound, "/database_not_available")
|
||||
func indexGet(ec echo.Context) error {
|
||||
htmlRaw, err := static.ReadFile("index.html")
|
||||
if err != nil {
|
||||
return ec.String(http.StatusNotFound, "index.html wasn't found!")
|
||||
}
|
||||
|
||||
// Generate list of available languages to highlight.
|
||||
availableLexers := lexers.Names(false)
|
||||
|
||||
availableLexersSelectOpts := "<option value='text'>Text</option><option value='autodetect'>Auto detect</option><option disabled>-----</option>"
|
||||
var availableLexersSelectOpts = "<option value='text'>Text</option><option value='autodetect'>Auto detect</option><option disabled>-----</option>"
|
||||
for i := range availableLexers {
|
||||
availableLexersSelectOpts += "<option value='" + availableLexers[i] + "'>" + availableLexers[i] + "</option>"
|
||||
}
|
||||
|
||||
html := strings.Replace(string(htmlRaw), "{lexers}", availableLexersSelectOpts, 1)
|
||||
|
||||
// Captcha.
|
||||
captchaString := captcha.NewCaptcha()
|
||||
html = strings.Replace(html, "{captchaString}", captchaString, -1)
|
||||
|
||||
htmlData := templater.GetTemplate(ectx, "index.html", map[string]string{"lexers": availableLexersSelectOpts, "captchaString": captchaString})
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusOK, htmlData)
|
||||
return ec.HTML(http.StatusOK, html)
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
// Code generated by fileb0x at "2018-05-18 22:25:13.180720675 +0500 +05 m=+0.030971779" from config file "fileb0x.yml" DO NOT EDIT.
|
||||
// modification hash(1679c5529785a2e624071b2679319ecb.608888160ddb195ed89fce5ab290ff92)
|
||||
|
||||
package static
|
||||
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
"context"
|
||||
|
||||
|
||||
)
|
||||
|
||||
var (
|
||||
// CTX is a context for webdav vfs
|
||||
CTX = context.Background()
|
||||
|
||||
|
||||
// FS is a virtual memory file system
|
||||
FS = webdav.NewMemFS()
|
||||
|
||||
|
||||
// Handler is used to server files through a http handler
|
||||
Handler *webdav.Handler
|
||||
|
||||
// HTTP is the http file system
|
||||
HTTP http.FileSystem = new(HTTPFS)
|
||||
)
|
||||
|
||||
// HTTPFS implements http.FileSystem
|
||||
type HTTPFS struct {}
|
||||
|
||||
|
||||
|
||||
func init() {
|
||||
if CTX.Err() != nil {
|
||||
panic(CTX.Err())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var err error
|
||||
|
||||
|
||||
|
||||
|
||||
err = FS.Mkdir(CTX, "static/", 0777)
|
||||
if err != nil && err != os.ErrExist {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
|
||||
err = FS.Mkdir(CTX, "static/css/", 0777)
|
||||
if err != nil && err != os.ErrExist {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
|
||||
err = FS.Mkdir(CTX, "static/js/", 0777)
|
||||
if err != nil && err != os.ErrExist {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Handler = &webdav.Handler{
|
||||
FileSystem: FS,
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Open a file
|
||||
func (hfs *HTTPFS) Open(path string) (http.File, error) {
|
||||
|
||||
|
||||
f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ReadFile is adapTed from ioutil
|
||||
func ReadFile(path string) ([]byte, error) {
|
||||
f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 0, bytes.MinRead))
|
||||
|
||||
// If the buffer overflows, we will get bytes.ErrTooLarge.
|
||||
// Return that as an error. Any other panic remains.
|
||||
defer func() {
|
||||
e := recover()
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
|
||||
err = panicErr
|
||||
} else {
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
_, err = buf.ReadFrom(f)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
// WriteFile is adapTed from ioutil
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||
f, err := FS.OpenFile(CTX, filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := f.Write(data)
|
||||
if err == nil && n < len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if err1 := f.Close(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// WalkDirs looks for files in the given dir and returns a list of files in it
|
||||
// usage for all files in the b0x: WalkDirs("", false)
|
||||
func WalkDirs(name string, includeDirsInList bool, files ...string) ([]string, error) {
|
||||
f, err := FS.OpenFile(CTX, name, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileInfos, err := f.Readdir(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, info := range fileInfos {
|
||||
filename := path.Join(name, info.Name())
|
||||
|
||||
if includeDirsInList || !info.IsDir() {
|
||||
files = append(files, filename)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
files, err = WalkDirs(filename, includeDirsInList, files...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Code generaTed by fileb0x at "2018-05-01 18:35:23.702577304 +0500 +05 m=+0.047872409" from config file "fileb0x.yml" DO NOT EDIT.
|
||||
// modified(2018-05-01 18:35:02 +0500 +05)
|
||||
// original path: assets/error.html
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
// FileErrorHTML is "/error.html"
|
||||
var FileErrorHTML = []byte("\x3c\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x74\x6d\x6c\x3e\x0a\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\x66\x2d\x38\x22\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x6e\x61\x6d\x65\x3d\x22\x76\x69\x65\x77\x70\x6f\x72\x74\x22\x20\x63\x6f\x6e\x74\x65\x6e\x74\x3d\x22\x77\x69\x64\x74\x68\x3d\x64\x65\x76\x69\x63\x65\x2d\x77\x69\x64\x74\x68\x2c\x20\x69\x6e\x69\x74\x69\x61\x6c\x2d\x73\x63\x61\x6c\x65\x3d\x31\x22\x3e\x0a\x20\x20\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x20\x2d\x20\x45\x52\x52\x4f\x52\x21\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x62\x75\x6c\x6d\x61\x2d\x30\x2e\x37\x2e\x30\x2e\x6d\x69\x6e\x2e\x63\x73\x73\x22\x3e\x0a\x20\x20\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x64\x65\x66\x65\x72\x20\x73\x72\x63\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x6a\x73\x2f\x66\x6f\x6e\x74\x61\x77\x65\x73\x6f\x6d\x65\x2d\x35\x2e\x30\x2e\x37\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x73\x74\x79\x6c\x65\x2e\x63\x73\x73\x22\x3e\x0a\x3c\x2f\x68\x65\x61\x64\x3e\x0a\x0a\x3c\x62\x6f\x64\x79\x3e\x0a\x20\x20\x20\x20\x3c\x6e\x61\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x20\x69\x73\x2d\x64\x61\x72\x6b\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x62\x72\x61\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x22\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x70\x61\x73\x74\x65\x73\x2f\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x50\x61\x73\x74\x65\x73\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x62\x75\x72\x67\x65\x72\x20\x62\x75\x72\x67\x65\x72\x22\x20\x64\x61\x74\x61\x2d\x74\x61\x72\x67\x65\x74\x3d\x22\x6e\x61\x76\x62\x61\x72\x49\x74\x65\x6d\x73\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x69\x64\x3d\x22\x6e\x61\x76\x62\x61\x72\x49\x74\x65\x6d\x73\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x6d\x65\x6e\x75\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x73\x74\x61\x72\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x65\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x6e\x61\x76\x3e\x0a\x20\x20\x20\x20\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x3e\x7b\x65\x72\x72\x6f\x72\x7d\x3c\x2f\x70\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e\x0a\x3c\x2f\x62\x6f\x64\x79\x3e\x0a\x0a\x3c\x2f\x68\x74\x6d\x6c\x3e")
|
||||
|
||||
func init() {
|
||||
|
||||
|
||||
f, err := FS.OpenFile(CTX, "/error.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
_, err = f.Write(FileErrorHTML)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,35 @@
|
|||
// Code generaTed by fileb0x at "2018-05-01 00:41:18.339400139 +0500 +05 m=+0.041230393" from config file "fileb0x.yml" DO NOT EDIT.
|
||||
// modified(2018-05-01 00:32:51 +0500 +05)
|
||||
// original path: assets/pagination.html
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
// FilePaginationHTML is "/pagination.html"
|
||||
var FilePaginationHTML = []byte("\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x3c\x6e\x61\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x20\x69\x73\x2d\x63\x65\x6e\x74\x65\x72\x65\x64\x22\x20\x72\x6f\x6c\x65\x3d\x22\x6e\x61\x76\x69\x67\x61\x74\x69\x6f\x6e\x22\x20\x61\x72\x69\x61\x2d\x6c\x61\x62\x65\x6c\x3d\x22\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x2d\x70\x72\x65\x76\x69\x6f\x75\x73\x22\x20\x68\x72\x65\x66\x3d\x22\x7b\x70\x72\x65\x76\x69\x6f\x75\x73\x50\x61\x67\x65\x4c\x69\x6e\x6b\x7d\x22\x3e\x50\x72\x65\x76\x69\x6f\x75\x73\x20\x70\x61\x67\x65\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x75\x6c\x20\x63\x6c\x61\x73\x73\x3d\x22\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x2d\x6c\x69\x73\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x4c\x69\x6e\x6b\x73\x7d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x75\x6c\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x2d\x6e\x65\x78\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x7b\x6e\x65\x78\x74\x50\x61\x67\x65\x4c\x69\x6e\x6b\x7d\x22\x3e\x4e\x65\x78\x74\x20\x70\x61\x67\x65\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x6e\x61\x76\x3e\x0a\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e")
|
||||
|
||||
func init() {
|
||||
|
||||
|
||||
f, err := FS.OpenFile(CTX, "/pagination.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
_, err = f.Write(FilePaginationHTML)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Code generaTed by fileb0x at "2018-04-30 23:41:29.363654393 +0500 +05 m=+0.033742092" from config file "fileb0x.yml" DO NOT EDIT.
|
||||
// modified(2018-04-30 23:39:44.086392054 +0500 +05)
|
||||
// original path: assets/pagination_ellipsis.html
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
// FilePaginationEllipsisHTML is "/pagination_ellipsis.html"
|
||||
var FilePaginationEllipsisHTML = []byte("\x3c\x6c\x69\x3e\x0a\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x2d\x65\x6c\x6c\x69\x70\x73\x69\x73\x22\x3e\x26\x68\x65\x6c\x6c\x69\x70\x3b\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x3c\x2f\x6c\x69\x3e")
|
||||
|
||||
func init() {
|
||||
|
||||
|
||||
f, err := FS.OpenFile(CTX, "/pagination_ellipsis.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
_, err = f.Write(FilePaginationEllipsisHTML)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Code generaTed by fileb0x at "2018-05-01 00:26:24.973087795 +0500 +05 m=+0.032533011" from config file "fileb0x.yml" DO NOT EDIT.
|
||||
// modified(2018-04-30 23:54:11 +0500 +05)
|
||||
// original path: assets/pagination_link.html
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
// FilePaginationLinkHTML is "/pagination_link.html"
|
||||
var FilePaginationLinkHTML = []byte("\x3c\x6c\x69\x3e\x0a\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x2d\x6c\x69\x6e\x6b\x22\x20\x61\x72\x69\x61\x2d\x6c\x61\x62\x65\x6c\x3d\x22\x47\x6f\x20\x74\x6f\x20\x70\x61\x67\x65\x20\x7b\x70\x61\x67\x65\x4e\x75\x6d\x7d\x22\x20\x68\x72\x65\x66\x3d\x22\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x4c\x69\x6e\x6b\x7d\x22\x3e\x7b\x70\x61\x67\x65\x4e\x75\x6d\x7d\x3c\x2f\x61\x3e\x0a\x3c\x2f\x6c\x69\x3e")
|
||||
|
||||
func init() {
|
||||
|
||||
|
||||
f, err := FS.OpenFile(CTX, "/pagination_link.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
_, err = f.Write(FilePaginationLinkHTML)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Code generaTed by fileb0x at "2018-05-01 00:26:24.971847954 +0500 +05 m=+0.031293214" from config file "fileb0x.yml" DO NOT EDIT.
|
||||
// modified(2018-04-30 23:54:14 +0500 +05)
|
||||
// original path: assets/pagination_link_current.html
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
// FilePaginationLinkCurrentHTML is "/pagination_link_current.html"
|
||||
var FilePaginationLinkCurrentHTML = []byte("\x3c\x6c\x69\x3e\x0a\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x2d\x6c\x69\x6e\x6b\x20\x69\x73\x2d\x63\x75\x72\x72\x65\x6e\x74\x22\x20\x61\x72\x69\x61\x2d\x6c\x61\x62\x65\x6c\x3d\x22\x50\x61\x67\x65\x20\x7b\x70\x61\x67\x65\x4e\x75\x6d\x7d\x22\x20\x61\x72\x69\x61\x2d\x63\x75\x72\x72\x65\x6e\x74\x3d\x22\x70\x61\x67\x65\x22\x3e\x7b\x70\x61\x67\x65\x4e\x75\x6d\x7d\x3c\x2f\x61\x3e\x0a\x3c\x2f\x6c\x69\x3e")
|
||||
|
||||
func init() {
|
||||
|
||||
|
||||
f, err := FS.OpenFile(CTX, "/pagination_link_current.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
_, err = f.Write(FilePaginationLinkCurrentHTML)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,35 @@
|
|||
// Code generaTed by fileb0x at "2018-05-18 22:25:13.191256649 +0500 +05 m=+0.041507879" from config file "fileb0x.yml" DO NOT EDIT.
|
||||
// modified(2018-05-18 22:17:55.447214662 +0500 +05)
|
||||
// original path: assets/pastelist_list.html
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
// FilePastelistListHTML is "/pastelist_list.html"
|
||||
var FilePastelistListHTML = []byte("\x3c\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x74\x6d\x6c\x3e\x0a\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\x66\x2d\x38\x22\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x6e\x61\x6d\x65\x3d\x22\x76\x69\x65\x77\x70\x6f\x72\x74\x22\x20\x63\x6f\x6e\x74\x65\x6e\x74\x3d\x22\x77\x69\x64\x74\x68\x3d\x64\x65\x76\x69\x63\x65\x2d\x77\x69\x64\x74\x68\x2c\x20\x69\x6e\x69\x74\x69\x61\x6c\x2d\x73\x63\x61\x6c\x65\x3d\x31\x22\x3e\x0a\x20\x20\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x20\x2d\x20\x50\x61\x73\x74\x65\x73\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x62\x75\x6c\x6d\x61\x2d\x30\x2e\x37\x2e\x30\x2e\x6d\x69\x6e\x2e\x63\x73\x73\x22\x3e\x0a\x20\x20\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x64\x65\x66\x65\x72\x20\x73\x72\x63\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x6a\x73\x2f\x66\x6f\x6e\x74\x61\x77\x65\x73\x6f\x6d\x65\x2d\x35\x2e\x30\x2e\x37\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x73\x74\x79\x6c\x65\x2e\x63\x73\x73\x22\x3e\x0a\x3c\x2f\x68\x65\x61\x64\x3e\x0a\x0a\x3c\x62\x6f\x64\x79\x3e\x0a\x20\x20\x20\x20\x3c\x6e\x61\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x20\x69\x73\x2d\x64\x61\x72\x6b\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x62\x72\x61\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x22\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x70\x61\x73\x74\x65\x73\x2f\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x50\x61\x73\x74\x65\x73\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x62\x75\x72\x67\x65\x72\x20\x62\x75\x72\x67\x65\x72\x22\x20\x64\x61\x74\x61\x2d\x74\x61\x72\x67\x65\x74\x3d\x22\x6e\x61\x76\x62\x61\x72\x49\x74\x65\x6d\x73\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x69\x64\x3d\x22\x6e\x61\x76\x62\x61\x72\x49\x74\x65\x6d\x73\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x6d\x65\x6e\x75\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x73\x74\x61\x72\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x65\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x6e\x61\x76\x3e\x0a\x20\x20\x20\x20\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x7d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x73\x74\x65\x73\x7d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x7d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e\x0a\x3c\x2f\x62\x6f\x64\x79\x3e\x0a\x0a\x3c\x2f\x68\x74\x6d\x6c\x3e")
|
||||
|
||||
func init() {
|
||||
|
||||
|
||||
f, err := FS.OpenFile(CTX, "/pastelist_list.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
_, err = f.Write(FilePastelistListHTML)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Code generaTed by fileb0x at "2018-05-01 18:35:23.706057355 +0500 +05 m=+0.051352478" from config file "fileb0x.yml" DO NOT EDIT.
|
||||
// modified(2018-05-01 18:35:21 +0500 +05)
|
||||
// original path: assets/pastelist_paste.html
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
// FilePastelistPasteHTML is "/pastelist_paste.html"
|
||||
var FilePastelistPasteHTML = []byte("\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6f\x6e\x74\x65\x6e\x74\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x61\x72\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x68\x65\x61\x64\x65\x72\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x61\x72\x64\x2d\x68\x65\x61\x64\x65\x72\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x61\x72\x64\x2d\x68\x65\x61\x64\x65\x72\x2d\x74\x69\x74\x6c\x65\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x50\x61\x73\x74\x65\x20\x23\x7b\x70\x61\x73\x74\x65\x49\x44\x7d\x2c\x20\x70\x6f\x73\x74\x65\x64\x20\x6f\x6e\x20\x7b\x70\x61\x73\x74\x65\x44\x61\x74\x65\x7d\x20\x61\x6e\x64\x20\x74\x69\x74\x6c\x65\x64\x20\x61\x73\x20\x22\x7b\x70\x61\x73\x74\x65\x54\x69\x74\x6c\x65\x7d\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x70\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x68\x65\x61\x64\x65\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x61\x72\x64\x2d\x63\x6f\x6e\x74\x65\x6e\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6f\x6e\x74\x65\x6e\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x72\x65\x3e\x7b\x70\x61\x73\x74\x65\x44\x61\x74\x61\x7d\x3c\x2f\x70\x72\x65\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x66\x6f\x6f\x74\x65\x72\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x61\x72\x64\x2d\x66\x6f\x6f\x74\x65\x72\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x2f\x70\x61\x73\x74\x65\x2f\x7b\x70\x61\x73\x74\x65\x49\x44\x7d\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x61\x72\x64\x2d\x66\x6f\x6f\x74\x65\x72\x2d\x69\x74\x65\x6d\x20\x62\x75\x74\x74\x6f\x6e\x20\x69\x73\x2d\x73\x75\x63\x63\x65\x73\x73\x20\x69\x73\x2d\x72\x61\x64\x69\x75\x73\x6c\x65\x73\x73\x22\x3e\x56\x69\x65\x77\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x66\x6f\x6f\x74\x65\x72\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x64\x69\x76\x3e")
|
||||
|
||||
func init() {
|
||||
|
||||
|
||||
f, err := FS.OpenFile(CTX, "/pastelist_paste.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
_, err = f.Write(FilePastelistPasteHTML)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,35 @@
|
|||
// Code generaTed by fileb0x at "2018-05-01 17:43:41.903107504 +0500 +05 m=+0.039828393" from config file "fileb0x.yml" DO NOT EDIT.
|
||||
// modified(2018-05-01 17:43:40.11480207 +0500 +05)
|
||||
// original path: assets/css/style.css
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
// FileStaticCSSStyleCSS is "static/css/style.css"
|
||||
var FileStaticCSSStyleCSS = []byte("\x23\x70\x61\x73\x74\x65\x2d\x63\x6f\x6e\x74\x65\x6e\x74\x73\x20\x7b\x0a\x20\x20\x20\x20\x66\x6f\x6e\x74\x2d\x66\x61\x6d\x69\x6c\x79\x3a\x20\x6d\x6f\x6e\x6f\x73\x70\x61\x63\x65\x3b\x0a\x20\x20\x20\x20\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\x20\x30\x2e\x39\x72\x65\x6d\x3b\x0a\x20\x20\x20\x20\x68\x65\x69\x67\x68\x74\x3a\x20\x39\x30\x76\x68\x3b\x0a\x7d\x0a\x0a\x2e\x70\x61\x73\x74\x65\x2d\x64\x61\x74\x61\x20\x7b\x0a\x20\x20\x20\x20\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\x20\x30\x2e\x39\x72\x65\x6d\x3b\x0a\x7d")
|
||||
|
||||
func init() {
|
||||
|
||||
|
||||
f, err := FS.OpenFile(CTX, "static/css/style.css", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
_, err = f.Write(FileStaticCSSStyleCSS)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -22,18 +22,19 @@
|
|||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package indexpage
|
||||
package json
|
||||
|
||||
import (
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/context"
|
||||
)
|
||||
|
||||
var ctx *context.Context
|
||||
var (
|
||||
c *context.Context
|
||||
)
|
||||
|
||||
// New initializes pastes package and adds necessary HTTP and API
|
||||
// endpoints.
|
||||
// New initializes basic JSON API.
|
||||
func New(cc *context.Context) {
|
||||
ctx = cc
|
||||
|
||||
ctx.Echo.GET("/", indexGet)
|
||||
c = cc
|
||||
c.Logger.Info().Msg("Initializing JSON API...")
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
//nolint:gofmt,gofumpt,goimports
|
||||
package assets
|
||||
|
||||
import "embed"
|
||||
|
||||
// Data is an embedded assets data.
|
||||
//go:embed *
|
||||
var Data embed.FS
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,9 @@
|
|||
#paste-contents {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.paste-data {
|
||||
font-size: 0.9rem;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
<section class="section">
|
||||
<div class="notification is-danger">
|
||||
<h3><strong>Database not available</strong></h3>
|
||||
<p>Something went wrong while trying to connect to database. Check logs for details.</p>
|
||||
</div>
|
||||
</section>
|
|
@ -1,5 +1,40 @@
|
|||
<section class="section">
|
||||
<div class="box has-background-warning">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Fast Paste Bin - ERROR!</title>
|
||||
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
|
||||
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar is-dark">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">Fast Paste Bin</a>
|
||||
<a class="navbar-item" href="/pastes/">
|
||||
Pastes
|
||||
</a>
|
||||
<div class="navbar-burger burger" data-target="navbarItems">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navbarItems" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="section">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,11 +0,0 @@
|
|||
<div class="container">
|
||||
<div class="content has-text-centered">
|
||||
<p>
|
||||
<strong>Fast paste bin</strong> version
|
||||
<strong>{version}</strong> by
|
||||
<a href="https://pztrn.name">Stanislav N. aka pztrn</a>. The source code is licensed
|
||||
<a href="http://opensource.org/licenses/mit-license.php">MIT</a>. Get
|
||||
<a href="https://code.pztrn.name/apps/fastpastebin">source or binary releases here</a>!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -1,81 +1,120 @@
|
|||
<section class="section">
|
||||
<form action="/paste/" method="POST" autocomplete="off">
|
||||
<div class="columns">
|
||||
<div class="column is-8 field">
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="Paste contents" name="paste-contents" id="paste-contents"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Paste title" name="paste-title" id="paste-title">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="paste-keep-for">Paste should be available for:</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select id="paste-keep-for" name="paste-keep-for">
|
||||
<option value="30M">30 minutes</option>
|
||||
<option value="1h">1 hour</option>
|
||||
<option value="3h">3 hours</option>
|
||||
<option value="6h">6 hours</option>
|
||||
<option value="12h">12 hours</option>
|
||||
<option value="1d">1 day</option>
|
||||
<option value="2d">2 days</option>
|
||||
<option value="3d">3 days</option>
|
||||
<option value="4d">4 days</option>
|
||||
<option value="5d">5 days</option>
|
||||
<option value="7d">7 days</option>
|
||||
<option value="14d">14 days</option>
|
||||
<option value="21d">21 days</option>
|
||||
<option value="1m">1 monts</option>
|
||||
<option value="3m">3 months</option>
|
||||
<option value="6m">6 months</option>
|
||||
<option value="forever">Forever</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tooltip is-tooltip-bottom is-tooltip-multiline has-tooltip-arrow" data-tooltip="Should this paste be accessible only with special URL and not shown in pastes list? WARNING: If you'll enter password into 'Password for paste' field this checkbox will be assumed as checked!">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="paste-private" id="paste-private"> Private paste with unique URL?
|
||||
</label>
|
||||
</div>
|
||||
<div>OR</div>
|
||||
<div class="field tooltip is-tooltip-bottom is-tooltip-multiline has-tooltip-arrow" data-tooltip="If you'll enter password here - 'Private paste with unique URL' checkbox will be assumed as checked.">
|
||||
<label for="paste-password">Password for paste:</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Enter password." name="paste-password" id="paste-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="paste-language">Language:</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select id="paste-language" name="paste-language">
|
||||
{lexers}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="paste-captcha">Captcha:</label>
|
||||
<div class="control">
|
||||
<p>
|
||||
<img id=image src="/captcha/{captchaString}.png" alt="Captcha image">
|
||||
</p>
|
||||
<input class="input" type="text" placeholder="Captcha solution" name="paste-captcha-solution" id="paste-captcha-solution">
|
||||
<input class="input is-hidden" type="text" name="paste-captcha-id" id="paste-captcha-id" readonly value="{captchaString}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="button is-success" type="submit" value="Paste!">
|
||||
</div>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Fast Paste Bin</title>
|
||||
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
|
||||
<link rel="stylesheet" href="/static/css/bulma-tooltip-1.0.4.min.css">
|
||||
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar is-dark">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">Fast Paste Bin</a>
|
||||
<a class="navbar-item" href="/pastes/">
|
||||
Pastes
|
||||
</a>
|
||||
<div class="navbar-burger burger" data-target="navbarItems">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div id="navbarItems" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="section">
|
||||
<form action="/paste/" method="POST" autocomplete="off">
|
||||
<div class="columns">
|
||||
<div class="column is-8 field">
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="Paste contents" name="paste-contents" id="paste-contents"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Paste title" name="paste-title" id="paste-title">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="paste-keep-for">Paste should be available for:</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select id="paste-keep-for" name="paste-keep-for">
|
||||
<option value="30M">30 minutes</option>
|
||||
<option value="1h">1 hour</option>
|
||||
<option value="3h">3 hours</option>
|
||||
<option value="6h">6 hours</option>
|
||||
<option value="12h">12 hours</option>
|
||||
<option value="1d">1 day</option>
|
||||
<option value="2d">2 days</option>
|
||||
<option value="3d">3 days</option>
|
||||
<option value="4d">4 days</option>
|
||||
<option value="5d">5 days</option>
|
||||
<option value="7d">7 days</option>
|
||||
<option value="14d">14 days</option>
|
||||
<option value="21d">21 days</option>
|
||||
<option value="1m">1 monts</option>
|
||||
<option value="3m">3 months</option>
|
||||
<option value="6m">6 months</option>
|
||||
<option value="forever">Forever</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tooltip is-tooltip-bottom is-tooltip-multiline" data-tooltip="Should this paste be accessible only with special URL and not shown in pastes list? WARNING: If you'll enter password into 'Password for paste' field this checkbox will be assumed as checked!">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="paste-private" id="paste-private"> Private paste with unique URL?
|
||||
</label>
|
||||
</div>
|
||||
<div>OR</div>
|
||||
<div class="field tooltip is-tooltip-bottom is-tooltip-multiline" data-tooltip="If you'll enter password here - 'Private
|
||||
paste with unique URL' checkbox will be assumed as checked.">
|
||||
<label for="paste-password">Password for paste:</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Enter password." name="paste-password" id="paste-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="paste-language">Language:</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select id="paste-language" name="paste-language">
|
||||
{lexers}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="paste-captcha">Captcha:</label>
|
||||
<div class="control">
|
||||
<p>
|
||||
<img id=image src="/captcha/{captchaString}.png" alt="Captcha image">
|
||||
</p>
|
||||
<input class="input" type="text" placeholder="Captcha solution" name="paste-captcha-solution" id="paste-captcha-solution">
|
||||
<input class="input is-hidden" type="text" name="paste-captcha-id" id="paste-captcha-id" readonly value="{captchaString}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="button is-success" type="submit" value="Paste!">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because one or more lines are too long
|
@ -1,20 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Fast Paste Bin</title>
|
||||
<link rel="stylesheet" href="/static/css/bulma-0.9.4.min.css">
|
||||
<link rel="stylesheet" href="/static/css/bulma-tooltip-1.2.min.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{navigation} {documentBody}
|
||||
</body>
|
||||
<footer class="footer">
|
||||
{footer}
|
||||
</footer>
|
||||
|
||||
</html>
|
|
@ -1,16 +0,0 @@
|
|||
<nav class="navbar is-dark">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">Fast Paste Bin</a>
|
||||
<a class="navbar-item" href="/pastes/">
|
||||
Pastes
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbarItems" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
|
@ -1,32 +1,69 @@
|
|||
<section class="section">
|
||||
<div class="content">
|
||||
<div class="columns">
|
||||
<div class="column is-4 is-offset-4">
|
||||
<form action="/paste/{pasteID}/{pasteTimestamp}/verify" method="POST" autocomplete="off">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
This paste is protected with password
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<div class="field tooltip is-tooltip-bottom is-tooltip-multiline" data-tooltip="If you'll enter password here - 'Private paste with unique URL' checkbox will be assumed as checked.">
|
||||
<label for="paste-password">Password for paste:</label>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Fast Paste Bin - Paste is protected with password</title>
|
||||
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
|
||||
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar is-dark">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">Fast Paste Bin</a>
|
||||
<a class="navbar-item" href="/pastes/">
|
||||
Pastes
|
||||
</a>
|
||||
<div class="navbar-burger burger" data-target="navbarItems">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navbarItems" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="section">
|
||||
<div class="content">
|
||||
<div class="columns">
|
||||
<div class="column is-4 is-offset-4">
|
||||
<form action="/paste/{pasteID}/{pasteTimestamp}/verify" method="POST" autocomplete="off">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
This paste is protected with password
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<div class="field tooltip is-tooltip-bottom is-tooltip-multiline" data-tooltip="If you'll enter password here - 'Private paste with unique URL' checkbox will be assumed as checked.">
|
||||
<label for="paste-password">Password for paste:</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Enter password." name="paste-password" id="paste-password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Enter password." name="paste-password" id="paste-password">
|
||||
<input class="button is-success" type="submit" value="Check password!">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="button is-success" type="submit" value="Check password!">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,34 +1,71 @@
|
|||
<section class="section">
|
||||
<div class="content">
|
||||
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Title</th>
|
||||
<th>Language</th>
|
||||
<th>Pasted on</th>
|
||||
<th>Will expire on</th>
|
||||
<th>Paste type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{pasteID}</td>
|
||||
<td>{pasteTitle}</td>
|
||||
<td>{pasteLanguage}</td>
|
||||
<td>{pasteDate}</td>
|
||||
<td>{pasteExpiration}</td>
|
||||
<td>{pasteType}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<a class="button" href="/paste/{pasteID}/{pasteTs}raw">View raw</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Fast Paste Bin - Paste #{pasteID}</title>
|
||||
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
|
||||
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar is-dark">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">Fast Paste Bin</a>
|
||||
<a class="navbar-item" href="/pastes/">
|
||||
Pastes
|
||||
</a>
|
||||
<div class="navbar-burger burger" data-target="navbarItems">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navbarItems" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="section">
|
||||
<div class="content">
|
||||
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Title</th>
|
||||
<th>Language</th>
|
||||
<th>Pasted on</th>
|
||||
<th>Will expire on</th>
|
||||
<th>Paste type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{pasteID}</td>
|
||||
<td>{pasteTitle}</td>
|
||||
<td>{pasteLanguage}</td>
|
||||
<td>{pasteDate}</td>
|
||||
<td>{pasteExpiration}</td>
|
||||
<td>{pasteType}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<a class="button" href="/paste/{pasteID}/{pasteTs}raw">View raw</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<div class="paste-data">
|
||||
{pastedata}
|
||||
</div>
|
||||
</section>
|
||||
<div class="paste-data">
|
||||
{pastedata}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,11 +1,48 @@
|
|||
<section class="section">
|
||||
<div>
|
||||
{pagination}
|
||||
</div>
|
||||
<div>
|
||||
{pastes}
|
||||
</div>
|
||||
<div>
|
||||
{pagination}
|
||||
</div>
|
||||
</section>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Fast Paste Bin - Pastes</title>
|
||||
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
|
||||
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar is-dark">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">Fast Paste Bin</a>
|
||||
<a class="navbar-item" href="/pastes/">
|
||||
Pastes
|
||||
</a>
|
||||
<div class="navbar-burger burger" data-target="navbarItems">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navbarItems" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="section">
|
||||
<div>
|
||||
{pagination}
|
||||
</div>
|
||||
<div>
|
||||
{pastes}
|
||||
</div>
|
||||
<div>
|
||||
{pagination}
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,14 +0,0 @@
|
|||
#paste-contents {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.paste-data {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* This is a special case for multiline tooltips. See https://github.com/Wikiki/bulma-tooltip/issues/34 */
|
||||
.tooltip.is-tooltip-multiline::before {
|
||||
white-space:pre-line
|
||||
}
|
26
builder.sh
26
builder.sh
|
@ -1,26 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
VERSION=$1
|
||||
if [ "${VERSION}" == "" ]; then
|
||||
echo "Specify version as first parameter!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OS_LIST=("darwin/amd64" "dragonfly/amd64" "freebsd/386" "freebsd/amd64" "freebsd/arm" "linux/386" "linux/amd64" "linux/arm" "linux/arm64" "linux/ppc64" "linux/ppc64le" "linux/mips" "linux/mipsle" "linux/mips64" "linux/mips64le" "linux/s390x" "netbsd/386" "netbsd/amd64" "netbsd/arm" "openbsd/386" "openbsd/amd64" "openbsd/arm" "solaris/amd64" "windows/386" "windows/amd64")
|
||||
|
||||
if [ ! -d ./dist ]; then
|
||||
mkdir -p ./dist
|
||||
fi
|
||||
|
||||
for os in ${OS_LIST[@]}; do
|
||||
mkdir -p ./dist/${os}
|
||||
goos=$(echo ${os} | awk -F"/" '{ print $1 }')
|
||||
goarch=$(echo ${os} | awk -F"/" '{ print $2 }')
|
||||
echo "Building for ${goos} ${goarch}..."
|
||||
GOOS=${goos} GOARCH=${goarch} GOFLAGS="-mod=vendor" go build -o ./dist/${os}/fastpastebin ./cmd/fastpastebin/
|
||||
cp ./examples/fastpastebin.yaml.dist ./dist/${os}/fastpastebin.yaml
|
||||
cd ./dist/${os}/
|
||||
tar -czf fastpastebin-${VERSION}-${goos}-${goarch}.tar.gz fastpastebin fastpastebin.yaml
|
||||
mv fastpastebin-${VERSION}-${goos}-${goarch}.tar.gz ../../
|
||||
cd - &>/dev/null
|
||||
done
|
|
@ -25,32 +25,31 @@
|
|||
package captcha
|
||||
|
||||
import (
|
||||
"github.com/dchest/captcha"
|
||||
"github.com/labstack/echo"
|
||||
"github.com/rs/zerolog"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/context"
|
||||
|
||||
// other
|
||||
"github.com/dchest/captcha"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *context.Context
|
||||
log zerolog.Logger
|
||||
c *context.Context
|
||||
)
|
||||
|
||||
// New initializes captcha package and adds necessary HTTP and API
|
||||
// New initializes captcha package and adds neccessary HTTP and API
|
||||
// endpoints.
|
||||
func New(cc *context.Context) {
|
||||
ctx = cc
|
||||
log = ctx.Logger.With().Str("type", "internal").Str("package", "captcha").Logger()
|
||||
c = cc
|
||||
|
||||
// New paste.
|
||||
ctx.Echo.GET("/captcha/:id.png", echo.WrapHandler(captcha.Server(captcha.StdWidth, captcha.StdHeight)))
|
||||
c.Echo.GET("/captcha/:id.png", echo.WrapHandler(captcha.Server(captcha.StdWidth, captcha.StdHeight)))
|
||||
}
|
||||
|
||||
// NewCaptcha creates new captcha string.
|
||||
func NewCaptcha() string {
|
||||
s := captcha.New()
|
||||
log.Debug().Str("captcha string", s).Msg("Created new captcha string")
|
||||
|
||||
c.Logger.Debug().Msgf("Created new captcha string: %s", s)
|
||||
return s
|
||||
}
|
||||
|
|
@ -25,54 +25,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
// stdlib
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"go.dev.pztrn.name/fastpastebin/domains/dbnotavailable"
|
||||
"go.dev.pztrn.name/fastpastebin/domains/indexpage"
|
||||
"go.dev.pztrn.name/fastpastebin/domains/pastes"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/captcha"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/templater"
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/api"
|
||||
"github.com/pztrn/fastpastebin/captcha"
|
||||
"github.com/pztrn/fastpastebin/context"
|
||||
"github.com/pztrn/fastpastebin/database"
|
||||
"github.com/pztrn/fastpastebin/database/migrations"
|
||||
"github.com/pztrn/fastpastebin/pastes"
|
||||
)
|
||||
|
||||
func main() {
|
||||
appCtx := context.New()
|
||||
appCtx.Initialize()
|
||||
c := context.New()
|
||||
c.Initialize()
|
||||
|
||||
appCtx.Logger.Info().Msg("Starting Fast Pastebin...")
|
||||
c.Logger.Info().Msg("Starting Fast Pastebin...")
|
||||
|
||||
// Here goes initial initialization for packages that want CLI flags
|
||||
// to be added.
|
||||
|
||||
// Parse flags.
|
||||
flag.Parse()
|
||||
c.Flagger.Parse()
|
||||
|
||||
// Continue loading.
|
||||
appCtx.LoadConfiguration()
|
||||
appCtx.InitializePost()
|
||||
database.New(appCtx)
|
||||
appCtx.Database.Initialize()
|
||||
templater.Initialize(appCtx)
|
||||
c.LoadConfiguration()
|
||||
database.New(c)
|
||||
c.Database.Initialize()
|
||||
migrations.New(c)
|
||||
migrations.Migrate()
|
||||
api.New(c)
|
||||
api.InitializeAPI()
|
||||
|
||||
captcha.New(appCtx)
|
||||
|
||||
dbnotavailable.New(appCtx)
|
||||
indexpage.New(appCtx)
|
||||
pastes.New(appCtx)
|
||||
captcha.New(c)
|
||||
pastes.New(c)
|
||||
|
||||
// CTRL+C handler.
|
||||
signalHandler := make(chan os.Signal, 1)
|
||||
shutdownDone := make(chan bool, 1)
|
||||
|
||||
signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-signalHandler
|
||||
appCtx.Shutdown()
|
||||
c.Shutdown()
|
||||
shutdownDone <- true
|
||||
}()
|
||||
|
||||
|
|
|
@ -24,10 +24,8 @@
|
|||
|
||||
package config
|
||||
|
||||
// Database describes database configuration.
|
||||
type Database struct {
|
||||
Type string `yaml:"type"`
|
||||
Path string `yaml:"path"`
|
||||
// ConfigDatabase describes database configuration.
|
||||
type ConfigDatabase struct {
|
||||
Address string `yaml:"address"`
|
||||
Port string `yaml:"port"`
|
||||
Username string `yaml:"username"`
|
|
@ -24,7 +24,9 @@
|
|||
|
||||
package config
|
||||
|
||||
// Pastes describes pastes subsystem configuration.
|
||||
type Pastes struct {
|
||||
Pagination int `yaml:"pagination"`
|
||||
// ConfigHTTP describes HTTP server configuration.
|
||||
type ConfigHTTP struct {
|
||||
Address string `yaml:"address"`
|
||||
Port string `yaml:"port"`
|
||||
AllowInsecure bool `yaml:"allow_insecure"`
|
||||
}
|
|
@ -24,9 +24,9 @@
|
|||
|
||||
package config
|
||||
|
||||
// Logging describes logger configuration.
|
||||
type Logging struct {
|
||||
// ConfigLogging describes logger configuration.
|
||||
type ConfigLogging struct {
|
||||
LogToFile bool `yaml:"log_to_file"`
|
||||
FileName string `yaml:"filename"`
|
||||
LogLevel string `yaml:"loglevel"`
|
||||
LogToFile bool `yaml:"log_to_file"`
|
||||
}
|
|
@ -24,10 +24,9 @@
|
|||
|
||||
package config
|
||||
|
||||
// Struct describes whole configuration.
|
||||
type Struct struct {
|
||||
Database Database `yaml:"database"`
|
||||
Logging Logging `yaml:"logging"`
|
||||
HTTP HTTP `yaml:"http"`
|
||||
Pastes Pastes `yaml:"pastes"`
|
||||
// ConfigStruct describes whole configuration.
|
||||
type ConfigStruct struct {
|
||||
Database ConfigDatabase `yaml:"database"`
|
||||
Logging ConfigLogging `yaml:"logging"`
|
||||
HTTP ConfigHTTP `yaml:"http"`
|
||||
}
|
|
@ -25,14 +25,19 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"flag"
|
||||
// stdlib
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/config"
|
||||
"github.com/pztrn/fastpastebin/database/interface"
|
||||
|
||||
// other
|
||||
"github.com/labstack/echo"
|
||||
"github.com/pztrn/flagger"
|
||||
"github.com/rs/zerolog"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/config"
|
||||
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
@ -41,24 +46,26 @@ import (
|
|||
// contains everything every part of application need, like configuration
|
||||
// access, logger, etc.
|
||||
type Context struct {
|
||||
Config *config.Struct
|
||||
Database databaseinterface.Interface
|
||||
Echo *echo.Echo
|
||||
Logger zerolog.Logger
|
||||
configPathFromCLI string
|
||||
Config *config.ConfigStruct
|
||||
Database databaseinterface.Interface
|
||||
Echo *echo.Echo
|
||||
Flagger *flagger.Flagger
|
||||
Logger zerolog.Logger
|
||||
}
|
||||
|
||||
// Initialize initializes context.
|
||||
func (c *Context) Initialize() {
|
||||
c.initializeLogger()
|
||||
c.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
|
||||
|
||||
flag.StringVar(&c.configPathFromCLI, "config", "NO_CONFIG", "Configuration file path. Can be overridded with FASTPASTEBIN_CONFIG environment variable.")
|
||||
}
|
||||
c.Flagger = flagger.New(nil)
|
||||
c.Flagger.Initialize()
|
||||
|
||||
// InitializePost initializes everything that needs a configuration.
|
||||
func (c *Context) InitializePost() {
|
||||
c.initializeLoggerPost()
|
||||
c.initializeHTTPServer()
|
||||
c.Flagger.AddFlag(&flagger.Flag{
|
||||
Name: "config",
|
||||
Description: "Configuration file path. Can be overridded with FASTPASTEBIN_CONFIG environment variable (this is what used in tests).",
|
||||
Type: "string",
|
||||
DefaultValue: "NO_CONFIG",
|
||||
})
|
||||
}
|
||||
|
||||
// LoadConfiguration loads configuration and executes right after Flagger
|
||||
|
@ -67,41 +74,42 @@ func (c *Context) InitializePost() {
|
|||
func (c *Context) LoadConfiguration() {
|
||||
c.Logger.Info().Msg("Loading configuration...")
|
||||
|
||||
configPath := c.configPathFromCLI
|
||||
var configPath = ""
|
||||
|
||||
// We're accepting configuration path from "-config" CLI parameter
|
||||
// and FASTPASTEBIN_CONFIG environment variable. Later have higher
|
||||
// weight and can override "-config" value.
|
||||
configPathFromCLI, err := c.Flagger.GetStringValue("config")
|
||||
configPathFromEnv, configPathFromEnvFound := os.LookupEnv("FASTPASTEBIN_CONFIG")
|
||||
if configPathFromEnvFound {
|
||||
configPath = configPathFromEnv
|
||||
}
|
||||
|
||||
if configPath == "NO_CONFIG" {
|
||||
if err != nil && configPathFromEnvFound || err == nil && configPathFromEnvFound {
|
||||
configPath = configPathFromEnv
|
||||
} else if err != nil && !configPathFromEnvFound || err == nil && configPathFromCLI == "NO_CONFIG" {
|
||||
c.Logger.Panic().Msg("Configuration file path wasn't passed via '-config' or 'FASTPASTEBIN_CONFIG' environment variable. Cannot continue.")
|
||||
} else if err == nil && !configPathFromEnvFound {
|
||||
configPath = configPathFromCLI
|
||||
}
|
||||
|
||||
// Normalize file path.
|
||||
normalizedConfigPath, err1 := filepath.Abs(configPath)
|
||||
if err1 != nil {
|
||||
c.Logger.Fatal().Err(err1).Msg("Failed to normalize path to configuration file")
|
||||
c.Logger.Fatal().Msgf("Failed to normalize path to configuration file: %s", err1.Error())
|
||||
}
|
||||
|
||||
c.Logger.Debug().Str("path", configPath).Msg("Configuration file path")
|
||||
c.Logger.Debug().Msgf("Configuration file path: %s", configPath)
|
||||
|
||||
//nolint:exhaustruct
|
||||
c.Config = &config.Struct{}
|
||||
c.Config = &config.ConfigStruct{}
|
||||
|
||||
// Read configuration file.
|
||||
fileData, err2 := os.ReadFile(normalizedConfigPath)
|
||||
fileData, err2 := ioutil.ReadFile(normalizedConfigPath)
|
||||
if err2 != nil {
|
||||
c.Logger.Panic().Err(err2).Msg("Failed to read configuration file")
|
||||
c.Logger.Panic().Msgf("Failed to read configuration file: %s", err2.Error())
|
||||
}
|
||||
|
||||
// Parse it into structure.
|
||||
err3 := yaml.Unmarshal(fileData, c.Config)
|
||||
if err3 != nil {
|
||||
c.Logger.Panic().Err(err3).Msg("Failed to parse configuration file")
|
||||
c.Logger.Panic().Msgf("Failed to parse configuration file: %s", err3.Error())
|
||||
}
|
||||
|
||||
// Yay! See what it gets!
|
||||
|
@ -121,6 +129,4 @@ func (c *Context) RegisterEcho(e *echo.Echo) {
|
|||
// Shutdown shutdowns entire application.
|
||||
func (c *Context) Shutdown() {
|
||||
c.Logger.Info().Msg("Shutting down Fast Pastebin...")
|
||||
|
||||
c.Database.Shutdown()
|
||||
}
|
|
@ -24,13 +24,7 @@
|
|||
|
||||
package context
|
||||
|
||||
const (
|
||||
// Version .
|
||||
Version = "0.4.1"
|
||||
)
|
||||
|
||||
// New creates new context.
|
||||
func New() *Context {
|
||||
//nolint:exhaustruct
|
||||
return &Context{}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"fmt"
|
||||
|
||||
// other
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// Database represents control structure for database connection.
|
||||
type Database struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
// GetDatabaseConnection returns current database connection.
|
||||
func (db *Database) GetDatabaseConnection() *sqlx.DB {
|
||||
return db.db
|
||||
}
|
||||
|
||||
// Initialize initializes connection to database.
|
||||
func (db *Database) Initialize() {
|
||||
c.Logger.Info().Msg("Initializing database connection...")
|
||||
|
||||
// There might be only user, without password. MySQL/MariaDB driver
|
||||
// in DSN wants "user" or "user:password", "user:" is invalid.
|
||||
var userpass = ""
|
||||
if c.Config.Database.Password == "" {
|
||||
userpass = c.Config.Database.Username
|
||||
} else {
|
||||
userpass = c.Config.Database.Username + ":" + c.Config.Database.Password
|
||||
}
|
||||
|
||||
dbConnString := fmt.Sprintf("%s@tcp(%s:%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci&charset=utf8mb4", userpass, c.Config.Database.Address, c.Config.Database.Port, c.Config.Database.Database)
|
||||
c.Logger.Debug().Msgf("Database connection string: %s", dbConnString)
|
||||
|
||||
dbConn, err := sqlx.Connect("mysql", dbConnString)
|
||||
if err != nil {
|
||||
c.Logger.Panic().Msgf("Failed to connect to database: %s", err.Error())
|
||||
}
|
||||
|
||||
// Force UTC for current connection.
|
||||
_ = dbConn.MustExec("SET @@session.time_zone='+00:00';")
|
||||
|
||||
c.Logger.Info().Msg("Database connection established")
|
||||
db.db = dbConn
|
||||
}
|
|
@ -25,20 +25,19 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/context"
|
||||
"github.com/pztrn/fastpastebin/database/interface"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *context.Context
|
||||
dbAdapter *Database
|
||||
c *context.Context
|
||||
d *Database
|
||||
)
|
||||
|
||||
// New initializes database structure.
|
||||
func New(cc *context.Context) {
|
||||
ctx = cc
|
||||
//nolint:exhaustruct
|
||||
dbAdapter = &Database{}
|
||||
|
||||
ctx.RegisterDatabaseInterface(databaseinterface.Interface(Handler{}))
|
||||
c = cc
|
||||
d = &Database{}
|
||||
c.RegisterDatabaseInterface(databaseinterface.Interface(Handler{}))
|
||||
}
|
|
@ -22,19 +22,23 @@
|
|||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package dbnotavailable
|
||||
package database
|
||||
|
||||
import (
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
// other
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var ctx *context.Context
|
||||
// Handler is an interfaceable structure that proxifies calls from anyone
|
||||
// to Database structure.
|
||||
type Handler struct{}
|
||||
|
||||
// New initializes pastes package and adds necessary HTTP and API
|
||||
// endpoints.
|
||||
func New(cc *context.Context) {
|
||||
ctx = cc
|
||||
|
||||
ctx.Echo.GET("/database_not_available", dbNotAvailableGet)
|
||||
ctx.Echo.GET("/database_not_available/raw", dbNotAvailableRawGet)
|
||||
// GetDatabaseConnection returns current database connection.
|
||||
func (dbh Handler) GetDatabaseConnection() *sqlx.DB {
|
||||
return d.GetDatabaseConnection()
|
||||
}
|
||||
|
||||
// Initialize initializes connection to database.
|
||||
func (dbh Handler) Initialize() {
|
||||
d.Initialize()
|
||||
}
|
|
@ -25,22 +25,13 @@
|
|||
package databaseinterface
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
// other
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// Interface represents database interface which is available to all
|
||||
// parts of application and registers with context.Context.
|
||||
type Interface interface {
|
||||
DeletePaste(int) error
|
||||
GetDatabaseConnection() *sql.DB
|
||||
GetPaste(pasteID int) (*structs.Paste, error)
|
||||
GetPagedPastes(page int) ([]structs.Paste, error)
|
||||
GetPastesPages() int
|
||||
GetDatabaseConnection() *sqlx.DB
|
||||
Initialize()
|
||||
RegisterDialect(dialectinterface.Interface)
|
||||
SavePaste(p *structs.Paste) (int64, error)
|
||||
Shutdown()
|
||||
}
|
|
@ -25,20 +25,13 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func InitialUp(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`CREATE TABLE pastes (
|
||||
id int(11) NOT NULL AUTO_INCREMENT COMMENT 'Paste ID',
|
||||
title text NOT NULL COMMENT 'Paste title',
|
||||
data longtext NOT NULL COMMENT 'Paste data',
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Paste creation timestamp',
|
||||
keep_for int(4) NOT NULL DEFAULT 1 COMMENT 'Keep for integer. 0 - forever.',
|
||||
keep_for_unit_type int(1) NOT NULL DEFAULT 1 COMMENT 'Keep for unit type. 1 - minutes, 2 - hours, 3 - days, 4 - months.',
|
||||
PRIMARY KEY (id), UNIQUE KEY id (id)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Pastes';`)
|
||||
_, err := tx.Exec("CREATE TABLE `pastes` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Paste ID', `title` text NOT NULL COMMENT 'Paste title', `data` longtext NOT NULL COMMENT 'Paste data', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Paste creation timestamp', `keep_for` int(4) NOT NULL DEFAULT 1 COMMENT 'Keep for integer. 0 - forever.', `keep_for_unit_type` int(1) NOT NULL DEFAULT 1 COMMENT 'Keep for unit type. 1 - minutes, 2 - hours, 3 - days, 4 - months.', PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Pastes';")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
|
@ -25,13 +25,13 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func PasteLangUp(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `pastes` ADD `language` VARCHAR(191) NOT NULL DEFAULT 'text' COMMENT 'Paste language'")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,6 @@ func PasteLangUp(tx *sql.Tx) error {
|
|||
func PasteLangDown(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `language`")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
|
@ -25,13 +25,13 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func PrivatePastesUp(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `pastes` ADD `private` BOOL NOT NULL DEFAULT false COMMENT 'Private paste? If true - additional URL parameter (UNIX TIMESTAMP) of paste will be required to access.'")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,6 @@ func PrivatePastesUp(tx *sql.Tx) error {
|
|||
func PrivatePastesDown(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `private`")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
|
@ -25,35 +25,32 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func PasswordedPastesUp(txn *sql.Tx) error {
|
||||
_, err := txn.Exec("ALTER TABLE `pastes` ADD `password` varchar(64) NOT NULL DEFAULT '' COMMENT 'Password for paste (scrypted and sha256ed).'")
|
||||
func PasswordedPastesUp(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `pastes` ADD `password` varchar(64) NOT NULL DEFAULT '' COMMENT 'Password for paste (scrypted and sha256ed).'")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
_, err1 := txn.Exec("ALTER TABLE `pastes` ADD `password_salt` varchar(64) NOT NULL DEFAULT '' COMMENT 'Password salt (sha256ed).'")
|
||||
_, err1 := tx.Exec("ALTER TABLE `pastes` ADD `password_salt` varchar(64) NOT NULL DEFAULT '' COMMENT 'Password salt (sha256ed).'")
|
||||
if err1 != nil {
|
||||
//nolint:wrapcheck
|
||||
return err1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PasswordedPastesDown(txn *sql.Tx) error {
|
||||
_, err := txn.Exec("ALTER TABLE `pastes` DROP COLUMN `password`")
|
||||
func PasswordedPastesDown(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `password`")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
_, err1 := txn.Exec("ALTER TABLE `pastes` DROP COLUMN `password_salt`")
|
||||
_, err1 := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `password_salt`")
|
||||
if err1 != nil {
|
||||
//nolint:wrapcheck
|
||||
return err1
|
||||
}
|
||||
|
|
@ -25,35 +25,37 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/context"
|
||||
|
||||
// other
|
||||
//"github.com/jmoiron/sqlx"
|
||||
"github.com/pressly/goose"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
)
|
||||
|
||||
var ctx *context.Context
|
||||
var (
|
||||
c *context.Context
|
||||
)
|
||||
|
||||
// New initializes migrations.
|
||||
func New(cc *context.Context) {
|
||||
ctx = cc
|
||||
c = cc
|
||||
}
|
||||
|
||||
// Migrate launching migrations.
|
||||
func Migrate() {
|
||||
ctx.Logger.Info().Msg("Migrating database...")
|
||||
c.Logger.Info().Msg("Migrating database...")
|
||||
|
||||
_ = goose.SetDialect("mysql")
|
||||
goose.SetDialect("mysql")
|
||||
goose.AddNamedMigration("1_initial.go", InitialUp, nil)
|
||||
goose.AddNamedMigration("2_paste_lang.go", PasteLangUp, PasteLangDown)
|
||||
goose.AddNamedMigration("3_private_pastes.go", PrivatePastesUp, PrivatePastesDown)
|
||||
goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown)
|
||||
// Add new migrations BEFORE this message.
|
||||
|
||||
dbConn := ctx.Database.GetDatabaseConnection()
|
||||
if dbConn != nil {
|
||||
err := goose.Up(dbConn, ".")
|
||||
if err != nil {
|
||||
ctx.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
ctx.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping")
|
||||
dbConn := c.Database.GetDatabaseConnection()
|
||||
err := goose.Up(dbConn.DB, ".")
|
||||
if err != nil {
|
||||
c.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error())
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
version: "3"
|
||||
|
||||
volumes:
|
||||
mysql:
|
||||
|
||||
services:
|
||||
database:
|
||||
image: mariadb:10.5
|
||||
container_name: database
|
||||
restart: always
|
||||
volumes:
|
||||
- fastpastebin_mysql:/var/lib/mysql
|
||||
environment:
|
||||
- MYSQL_RANDOM_ROOT_PASSWORD=yes
|
||||
- MYSQL_DATABASE=fastpastebin
|
||||
- MYSQL_USER=fastpastebin
|
||||
- MYSQL_PASSWORD=fastpastebin
|
||||
fastpastebin:
|
||||
build: .
|
||||
image: fastpastebin
|
||||
container_name: fastpastebin
|
||||
restart: always
|
||||
depends_on:
|
||||
- database
|
||||
volumes:
|
||||
- ./docker/fastpastebin.docker.yaml:/app/fastpastebin.yaml:ro
|
||||
# ports:
|
||||
# - 25544:25544
|
||||
web:
|
||||
image: nginx:1.16-alpine
|
||||
container_name: nginx
|
||||
volumes:
|
||||
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
ports:
|
||||
- 8080:80
|
||||
depends_on:
|
||||
- fastpastebin
|
|
@ -1,22 +0,0 @@
|
|||
database:
|
||||
type: "mysql"
|
||||
path: "./data"
|
||||
address: "database"
|
||||
port: "3306"
|
||||
username: "fastpastebin"
|
||||
password: "fastpastebin"
|
||||
database: "fastpastebin"
|
||||
|
||||
logging:
|
||||
log_to_file: false
|
||||
filename: ""
|
||||
# Log level. Acceptable parameters: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
|
||||
loglevel: "DEBUG"
|
||||
|
||||
http:
|
||||
address: "0.0.0.0"
|
||||
port: "25544"
|
||||
allow_insecure: true
|
||||
|
||||
pastes:
|
||||
pagination: 10
|
|
@ -1,10 +0,0 @@
|
|||
server {
|
||||
listen 80 default_server;
|
||||
location / {
|
||||
proxy_pass http://fastpastebin:25544;
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $CI_BUILD_REF_NAME == "master" ]]; then
|
||||
export DOCKER_TAG=latest;
|
||||
else
|
||||
export DOCKER_TAG="${CI_BUILD_REF_NAME}";
|
||||
fi
|
|
@ -1,356 +0,0 @@
|
|||
package pastes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
htmlfmt "github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/labstack/echo"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/flatfiles"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/templater"
|
||||
)
|
||||
|
||||
const (
|
||||
pasteCookieInvalid = "PASTE_COOKIE_INVALID"
|
||||
pasteExpired = "PASTE_EXPIRED"
|
||||
pasteNotFound = "PASTE_NOT_FOUND"
|
||||
pasteTimestampInvalid = "PASTE_TIMESTAMP_INVALID"
|
||||
)
|
||||
|
||||
// Actual getting paste data and returns it's content without formatting.
|
||||
// This function will return paste's structure and optional error string
|
||||
// that defined in constants above.
|
||||
// Actually required only paste ID, all other parameters are optional
|
||||
// for some cases, e.g. public paste won't check for timestamp and cookie
|
||||
// value (they both will be ignored), but private will.
|
||||
func pasteGetData(pasteID int, timestamp int64, cookieValue string) (*structs.Paste, string) {
|
||||
// Get paste.
|
||||
paste, err1 := ctx.Database.GetPaste(pasteID)
|
||||
if err1 != nil {
|
||||
ctx.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste")
|
||||
|
||||
return nil, pasteNotFound
|
||||
}
|
||||
|
||||
// Check if paste is expired.
|
||||
if paste.IsExpired() {
|
||||
ctx.Logger.Error().Int("paste ID", pasteID).Msg("Paste is expired")
|
||||
|
||||
return nil, pasteExpired
|
||||
}
|
||||
|
||||
// Check if we have a private paste and it's parameters are correct.
|
||||
if paste.Private {
|
||||
pasteTS := paste.CreatedAt.Unix()
|
||||
if timestamp != pasteTS {
|
||||
ctx.Logger.Error().Int("paste ID", pasteID).Int64("paste timestamp", pasteTS).Int64("provided timestamp", timestamp).Msg("Incorrect timestamp provided for private paste")
|
||||
|
||||
return nil, pasteTimestampInvalid
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a private paste requested and password for that paste
|
||||
// was defined - check additional things that required to view this
|
||||
// paste.
|
||||
if paste.Private && paste.Password != "" {
|
||||
// Generate cookie value to check.
|
||||
pasteCookieValue := paste.GenerateCryptedCookieValue()
|
||||
|
||||
if cookieValue != pasteCookieValue {
|
||||
return nil, pasteCookieInvalid
|
||||
}
|
||||
}
|
||||
|
||||
return paste, ""
|
||||
}
|
||||
|
||||
// GET for "/paste/PASTE_ID" and "/paste/PASTE_ID/TIMESTAMP" (private pastes).
|
||||
// Web interface version.
|
||||
func pasteGETWebInterface(ectx echo.Context) error {
|
||||
pasteIDRaw := ectx.Param("id")
|
||||
// We already get numbers from string, so we will not check strconv.Atoi()
|
||||
// error.
|
||||
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
||||
pasteIDStr := strconv.Itoa(pasteID)
|
||||
ctx.Logger.Debug().Int("paste ID", pasteID).Msg("Trying to get paste data")
|
||||
|
||||
// Check if we have timestamp passed.
|
||||
// If passed timestamp is invalid (isn't a real UNIX timestamp) we
|
||||
// will show 404 Not Found error and spam about that in logs.
|
||||
var timestamp int64
|
||||
|
||||
tsProvidedStr := ectx.Param("timestamp")
|
||||
if tsProvidedStr != "" {
|
||||
tsProvided, err := strconv.ParseInt(tsProvidedStr, 10, 64)
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Msg("Invalid timestamp provided for getting private paste")
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Paste #"+pasteIDStr+" not found")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
timestamp = tsProvided
|
||||
}
|
||||
|
||||
// Check if we have "PASTE-PASTEID" cookie defined. It is required
|
||||
// for private pastes.
|
||||
var cookieValue string
|
||||
|
||||
cookie, err1 := ectx.Cookie("PASTE-" + pasteIDStr)
|
||||
if err1 == nil {
|
||||
cookieValue = cookie.Value
|
||||
}
|
||||
|
||||
paste, err := pasteGetData(pasteID, timestamp, cookieValue)
|
||||
|
||||
// For these cases we should return 404 Not Found page.
|
||||
if err == pasteExpired || err == pasteNotFound || err == pasteTimestampInvalid {
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Paste #"+pasteIDRaw+" not found")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusNotFound, errtpl)
|
||||
}
|
||||
|
||||
// If passed cookie value was invalid - go to paste authorization
|
||||
// page.
|
||||
if err == pasteCookieInvalid {
|
||||
ctx.Logger.Info().Int("paste ID", pasteID).Msg("Invalid cookie, redirecting to auth page")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDStr+"/"+ectx.Param("timestamp")+"/verify")
|
||||
}
|
||||
|
||||
// Format paste data map.
|
||||
pasteData := make(map[string]string)
|
||||
pasteData["pasteTitle"] = paste.Title
|
||||
pasteData["pasteID"] = strconv.Itoa(paste.ID)
|
||||
pasteData["pasteDate"] = paste.CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
|
||||
pasteData["pasteLanguage"] = paste.Language
|
||||
|
||||
pasteExpirationString := "Never"
|
||||
if paste.KeepFor != 0 && paste.KeepForUnitType != 0 {
|
||||
pasteExpirationString = paste.GetExpirationTime().Format("2006-01-02 @ 15:04:05") + " UTC"
|
||||
}
|
||||
|
||||
pasteData["pasteExpiration"] = pasteExpirationString
|
||||
|
||||
if paste.Private {
|
||||
pasteData["pasteType"] = "<span class='has-text-danger'>Private</span>"
|
||||
pasteData["pasteTs"] = strconv.FormatInt(paste.CreatedAt.Unix(), 10) + "/"
|
||||
} else {
|
||||
pasteData["pasteType"] = "<span class='has-text-success'>Public</span>"
|
||||
pasteData["pasteTs"] = ""
|
||||
}
|
||||
|
||||
// Highlight.
|
||||
// Get lexer.
|
||||
lexer := lexers.Get(paste.Language)
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
// Tokenize paste data.
|
||||
lexered, err3 := lexer.Tokenise(nil, paste.Data)
|
||||
if err3 != nil {
|
||||
ctx.Logger.Error().Err(err3).Msg("Failed to tokenize paste data")
|
||||
}
|
||||
// Get style for HTML output.
|
||||
style := styles.Get("monokai")
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
// Get HTML formatter.
|
||||
formatter := htmlfmt.New(htmlfmt.WithLineNumbers(true), htmlfmt.LineNumbersInTable(true), htmlfmt.LinkableLineNumbers(true, "L"))
|
||||
|
||||
// Create buffer and format into it.
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
err4 := formatter.Format(buf, style, lexered)
|
||||
if err4 != nil {
|
||||
ctx.Logger.Error().Err(err4).Msg("Failed to format paste data")
|
||||
}
|
||||
|
||||
pasteData["pastedata"] = buf.String()
|
||||
|
||||
// Get template and format it.
|
||||
pasteHTML := templater.GetTemplate(ectx, "paste.html", pasteData)
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusOK, pasteHTML)
|
||||
}
|
||||
|
||||
// GET for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
|
||||
func pastePasswordedVerifyGet(ectx echo.Context) error {
|
||||
pasteIDRaw := ectx.Param("id")
|
||||
timestampRaw := ectx.Param("timestamp")
|
||||
// We already get numbers from string, so we will not check strconv.Atoi()
|
||||
// error.
|
||||
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
||||
|
||||
// Get paste.
|
||||
paste, err1 := ctx.Database.GetPaste(pasteID)
|
||||
if err1 != nil {
|
||||
ctx.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste data")
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Paste #"+pasteIDRaw+" not found")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
// Check for auth cookie. If present - redirect to paste.
|
||||
cookie, err := ectx.Cookie("PASTE-" + strconv.Itoa(pasteID))
|
||||
if err == nil {
|
||||
// No cookie, redirect to auth page.
|
||||
ctx.Logger.Debug().Msg("Paste cookie found, checking it...")
|
||||
|
||||
// Generate cookie value to check.
|
||||
cookieValue := paste.GenerateCryptedCookieValue()
|
||||
|
||||
if cookieValue == cookie.Value {
|
||||
ctx.Logger.Info().Msg("Valid cookie, redirecting to paste page...")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ectx.Param("timestamp"))
|
||||
}
|
||||
|
||||
ctx.Logger.Debug().Msg("Invalid cookie, showing auth page")
|
||||
}
|
||||
|
||||
// HTML data.
|
||||
htmlData := make(map[string]string)
|
||||
htmlData["pasteID"] = strconv.Itoa(pasteID)
|
||||
htmlData["pasteTimestamp"] = timestampRaw
|
||||
|
||||
verifyHTML := templater.GetTemplate(ectx, "passworded_paste_verify.html", htmlData)
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusOK, verifyHTML)
|
||||
}
|
||||
|
||||
// POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
|
||||
func pastePasswordedVerifyPost(ectx echo.Context) error {
|
||||
// We should check if database connection available.
|
||||
dbConn := ctx.Database.GetDatabaseConnection()
|
||||
|
||||
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusFound, "/database_not_available")
|
||||
}
|
||||
|
||||
pasteIDRaw := ectx.Param("id")
|
||||
timestampRaw := ectx.Param("timestamp")
|
||||
// We already get numbers from string, so we will not check strconv.Atoi()
|
||||
// error.
|
||||
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
||||
ctx.Logger.Debug().Int("paste ID", pasteID).Msg("Requesting paste")
|
||||
|
||||
// Get paste.
|
||||
paste, err1 := ctx.Database.GetPaste(pasteID)
|
||||
if err1 != nil {
|
||||
ctx.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste")
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
params, err2 := ectx.FormParams()
|
||||
if err2 != nil {
|
||||
ctx.Logger.Debug().Msg("No form parameters passed")
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
if paste.VerifyPassword(params["paste-password"][0]) {
|
||||
// Set cookie that this paste's password is verified and paste
|
||||
// can be viewed.
|
||||
cookie := new(http.Cookie)
|
||||
cookie.Name = "PASTE-" + strconv.Itoa(pasteID)
|
||||
cookie.Value = paste.GenerateCryptedCookieValue()
|
||||
cookie.Expires = time.Now().Add(24 * time.Hour)
|
||||
ectx.SetCookie(cookie)
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusFound, "/paste/"+strconv.Itoa(pasteID)+"/"+timestampRaw)
|
||||
}
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Invalid password. Please, try again.")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
// GET for "/pastes/:id/raw", raw paste output.
|
||||
// Web interface version.
|
||||
func pasteRawGETWebInterface(ectx echo.Context) error {
|
||||
// We should check if database connection available.
|
||||
dbConn := ctx.Database.GetDatabaseConnection()
|
||||
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusFound, "/database_not_available/raw")
|
||||
}
|
||||
|
||||
pasteIDRaw := ectx.Param("id")
|
||||
// We already get numbers from string, so we will not check strconv.Atoi()
|
||||
// error.
|
||||
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
||||
ctx.Logger.Debug().Int("paste ID", pasteID).Msg("Requesting paste data")
|
||||
|
||||
// Get paste.
|
||||
paste, err1 := ctx.Database.GetPaste(pasteID)
|
||||
if err1 != nil {
|
||||
ctx.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste from database")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
|
||||
}
|
||||
|
||||
if paste.IsExpired() {
|
||||
ctx.Logger.Error().Int("paste ID", pasteID).Msg("Paste is expired")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
|
||||
}
|
||||
|
||||
// Check if we have a private paste and it's parameters are correct.
|
||||
if paste.Private {
|
||||
tsProvidedStr := ectx.Param("timestamp")
|
||||
|
||||
tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64)
|
||||
if err2 != nil {
|
||||
ctx.Logger.Error().Err(err2).Int("paste ID", pasteID).Str("provided timestamp", tsProvidedStr).Msg("Invalid timestamp provided for getting private paste")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
|
||||
}
|
||||
|
||||
pasteTS := paste.CreatedAt.Unix()
|
||||
if tsProvided != pasteTS {
|
||||
ctx.Logger.Error().Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Int64("paste timestamp", pasteTS).Msg("Incorrect timestamp provided for private paste")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
|
||||
}
|
||||
}
|
||||
|
||||
//nolint
|
||||
// ToDo: figure out how to handle passworded pastes here.
|
||||
// Return error for now.
|
||||
if paste.Password != "" {
|
||||
ctx.Logger.Error().Int("paste ID", pasteID).Msg("Cannot render paste as raw: passworded paste. Patches welcome!")
|
||||
return ectx.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
|
||||
}
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.String(http.StatusOK, paste.Data)
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
package pastes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/labstack/echo"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/captcha"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/flatfiles"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/templater"
|
||||
)
|
||||
|
||||
const KeepPastesForever = "forever"
|
||||
|
||||
// POST for "/paste/" which will create new paste and redirect to
|
||||
// "/pastes/CREATED_PASTE_ID". This handler will do all the job for
|
||||
// requests comes from browsers via web interface.
|
||||
func pastePOSTWebInterface(ectx echo.Context) error {
|
||||
// We should check if database connection available.
|
||||
dbConn := ctx.Database.GetDatabaseConnection()
|
||||
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusFound, "/database_not_available")
|
||||
}
|
||||
|
||||
params, err := ectx.FormParams()
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Msg("Passed paste form is empty")
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Cannot create empty paste")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
ctx.Logger.Debug().Msgf("Received parameters: %+v", params)
|
||||
|
||||
// Do nothing if paste contents is empty.
|
||||
if len(params["paste-contents"][0]) == 0 {
|
||||
ctx.Logger.Debug().Msg("Empty paste submitted, ignoring")
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Empty pastes aren't allowed.")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
if !strings.ContainsAny(params["paste-keep-for"][0], "Mmhd") && params["paste-keep-for"][0] != KeepPastesForever {
|
||||
ctx.Logger.Debug().Str("field value", params["paste-keep-for"][0]).Msg("'Keep paste for' field have invalid value")
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
// Verify captcha.
|
||||
if !captcha.Verify(params["paste-captcha-id"][0], params["paste-captcha-solution"][0]) {
|
||||
ctx.Logger.Debug().Str("captcha ID", params["paste-captcha-id"][0]).Str("captcha solution", params["paste-captcha-solution"][0]).Msg("Invalid captcha solution")
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Invalid captcha solution.")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
//nolint:exhaustruct
|
||||
paste := &structs.Paste{
|
||||
Title: params["paste-title"][0],
|
||||
Data: params["paste-contents"][0],
|
||||
Language: params["paste-language"][0],
|
||||
}
|
||||
|
||||
// Paste creation time in UTC.
|
||||
createdAt := time.Now().UTC()
|
||||
paste.CreatedAt = &createdAt
|
||||
|
||||
// Parse "keep for" field.
|
||||
// Defaulting to "forever".
|
||||
keepFor := 0
|
||||
keepForUnit := 0
|
||||
|
||||
if params["paste-keep-for"][0] != KeepPastesForever {
|
||||
keepForUnitRegex := regexp.MustCompile("[Mmhd]")
|
||||
|
||||
keepForRaw := regexInts.FindAllString(params["paste-keep-for"][0], 1)[0]
|
||||
|
||||
var err error
|
||||
|
||||
keepFor, err = strconv.Atoi(keepForRaw)
|
||||
if err != nil {
|
||||
if params["paste-keep-for"][0] == KeepPastesForever {
|
||||
ctx.Logger.Debug().Msg("Keeping paste forever!")
|
||||
|
||||
keepFor = 0
|
||||
} else {
|
||||
ctx.Logger.Debug().Err(err).Msg("Failed to parse 'Keep for' integer")
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
}
|
||||
|
||||
keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0]
|
||||
keepForUnit = structs.PasteKeepsCorrelation[keepForUnitRaw]
|
||||
}
|
||||
|
||||
paste.KeepFor = keepFor
|
||||
paste.KeepForUnitType = keepForUnit
|
||||
|
||||
// Try to autodetect if it was selected.
|
||||
if params["paste-language"][0] == "autodetect" {
|
||||
lexer := lexers.Analyse(params["paste-language"][0])
|
||||
if lexer != nil {
|
||||
paste.Language = lexer.Config().Name
|
||||
} else {
|
||||
paste.Language = "text"
|
||||
}
|
||||
}
|
||||
|
||||
// Private paste?
|
||||
paste.Private = false
|
||||
privateCheckbox, privateCheckboxFound := params["paste-private"]
|
||||
pastePassword, pastePasswordFound := params["paste-password"]
|
||||
|
||||
if privateCheckboxFound && privateCheckbox[0] == "on" || pastePasswordFound && pastePassword[0] != "" {
|
||||
paste.Private = true
|
||||
}
|
||||
|
||||
if pastePassword[0] != "" {
|
||||
_ = paste.CreatePassword(pastePassword[0])
|
||||
}
|
||||
|
||||
pasteID, err2 := ctx.Database.SavePaste(paste)
|
||||
if err2 != nil {
|
||||
ctx.Logger.Error().Err(err2).Msg("Failed to save paste")
|
||||
|
||||
errtpl := templater.GetErrorTemplate(ectx, "Failed to save paste. Please, try again later.")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusBadRequest, errtpl)
|
||||
}
|
||||
|
||||
newPasteIDAsString := strconv.FormatInt(pasteID, 10)
|
||||
ctx.Logger.Debug().Msg("Paste saved, URL: /paste/" + newPasteIDAsString)
|
||||
|
||||
// Private pastes have it's timestamp in URL.
|
||||
if paste.Private {
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString+"/"+strconv.FormatInt(paste.CreatedAt.Unix(), 10))
|
||||
}
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString)
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package pastes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/flatfiles"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/pagination"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/templater"
|
||||
)
|
||||
|
||||
// GET for "/pastes/", a list of publicly available pastes.
|
||||
// Web interface version.
|
||||
func pastesGET(ectx echo.Context) error {
|
||||
// We should check if database connection available.
|
||||
dbConn := ctx.Database.GetDatabaseConnection()
|
||||
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
|
||||
//nolint:wrapcheck
|
||||
return ectx.Redirect(http.StatusFound, "/database_not_available")
|
||||
}
|
||||
|
||||
pageFromParamRaw := ectx.Param("page")
|
||||
|
||||
page := 1
|
||||
|
||||
if pageFromParamRaw != "" {
|
||||
pageRaw := regexInts.FindAllString(pageFromParamRaw, 1)[0]
|
||||
page, _ = strconv.Atoi(pageRaw)
|
||||
}
|
||||
|
||||
ctx.Logger.Debug().Int("page", page).Msg("Requested page")
|
||||
|
||||
// Get pastes IDs.
|
||||
pastes, err3 := ctx.Database.GetPagedPastes(page)
|
||||
ctx.Logger.Debug().Int("count", len(pastes)).Msg("Got pastes")
|
||||
|
||||
pastesString := "No pastes to show."
|
||||
|
||||
// Show "No pastes to show" on any error for now.
|
||||
if err3 != nil {
|
||||
ctx.Logger.Error().Err(err3).Msg("Failed to get pastes list from database")
|
||||
|
||||
noPastesToShowTpl := templater.GetErrorTemplate(ectx, "No pastes to show.")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusOK, noPastesToShowTpl)
|
||||
}
|
||||
|
||||
if len(pastes) > 0 {
|
||||
pastesString = ""
|
||||
|
||||
for _, paste := range pastes {
|
||||
pasteDataMap := make(map[string]string)
|
||||
pasteDataMap["pasteID"] = strconv.Itoa(paste.ID)
|
||||
pasteDataMap["pasteTitle"] = paste.Title
|
||||
pasteDataMap["pasteDate"] = paste.CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
|
||||
|
||||
// Get max 4 lines of each paste.
|
||||
pasteDataSplitted := strings.Split(paste.Data, "\n")
|
||||
|
||||
var pasteData string
|
||||
|
||||
if len(pasteDataSplitted) < 4 {
|
||||
pasteData = paste.Data
|
||||
} else {
|
||||
pasteData = strings.Join(pasteDataSplitted[0:4], "\n")
|
||||
}
|
||||
|
||||
pasteDataMap["pasteData"] = pasteData
|
||||
pasteTpl := templater.GetRawTemplate(ectx, "pastelist_paste.html", pasteDataMap)
|
||||
|
||||
pastesString += pasteTpl
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination.
|
||||
pages := ctx.Database.GetPastesPages()
|
||||
ctx.Logger.Debug().Int("total pages", pages).Int("current page", page).Msg("Paging data")
|
||||
paginationHTML := pagination.CreateHTML(page, pages, "/pastes/")
|
||||
|
||||
pasteListTpl := templater.GetTemplate(ectx, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML})
|
||||
|
||||
//nolint:wrapcheck
|
||||
return ectx.HTML(http.StatusOK, pasteListTpl)
|
||||
}
|
|
@ -1,13 +1,6 @@
|
|||
# Database configuration.
|
||||
# Only MySQL database and flatfiles are supported for now.
|
||||
# Only MySQL database is supported for now.
|
||||
database:
|
||||
# Database type. The only supported ATM is "mysql" and "flatfiles".
|
||||
type: "flatfiles"
|
||||
# Path for data stored with "flatfiles" database adapter.
|
||||
# Will be completely ignored for MySQL/MariaDB.
|
||||
path: "./data"
|
||||
# Next parameters are strictly for MySQL/MariaDB connections and
|
||||
# will be ignored by "flatfiles" adapter.
|
||||
address: "localhost"
|
||||
port: "3306"
|
||||
username: "fastpastebin"
|
||||
|
@ -25,16 +18,9 @@ logging:
|
|||
|
||||
# HTTP server configuration.
|
||||
http:
|
||||
address: "127.0.0.1"
|
||||
address: "localhost"
|
||||
port: "25544"
|
||||
# By default we're allowing only HTTPS requests. Setting this to true
|
||||
# will allow HTTP requests. Useful for developing or if you're
|
||||
# running Fast Pastebin behind reverse proxy that does SSL termination.
|
||||
allow_insecure: true
|
||||
# Maximum body size in megabytes. 1 should be enough for most use cases.
|
||||
max_body_size_megabytes: 1
|
||||
|
||||
# Pastes configuration.
|
||||
pastes:
|
||||
# Pastes per page.
|
||||
pagination: 10
|
||||
allow_insecure: false
|
|
@ -0,0 +1,108 @@
|
|||
# all folders and files are relative to the path
|
||||
# where fileb0x was run at!
|
||||
|
||||
# default: main
|
||||
pkg: static
|
||||
|
||||
# destination
|
||||
dest: "./api/http/static/"
|
||||
|
||||
# gofmt
|
||||
# type: bool
|
||||
# default: false
|
||||
fmt: false
|
||||
|
||||
# compress files
|
||||
# at the moment, only supports gzip
|
||||
#
|
||||
# type: object
|
||||
compression:
|
||||
# activates the compression
|
||||
#
|
||||
# type: bool
|
||||
# default: false
|
||||
compress: false
|
||||
|
||||
# valid values are:
|
||||
# -> "NoCompression"
|
||||
# -> "BestSpeed"
|
||||
# -> "BestCompression"
|
||||
# -> "DefaultCompression" or ""
|
||||
#
|
||||
# type: string
|
||||
# default: "DefaultCompression" # when: Compress == true && Method == ""
|
||||
method: ""
|
||||
|
||||
# true = do it yourself (the file is written as gzip compressed file into the memory file system)
|
||||
# false = decompress files at run time (while writing file into memory file system)
|
||||
#
|
||||
# type: bool
|
||||
# default: false
|
||||
keep: false
|
||||
|
||||
# ---------------
|
||||
# -- DANGEROUS --
|
||||
# ---------------
|
||||
#
|
||||
# cleans the destination folder (only b0xfiles)
|
||||
# you should use this when using the spread function
|
||||
# type: bool
|
||||
# default: false
|
||||
clean: true
|
||||
|
||||
# default: ab0x.go
|
||||
output: "ab0x.go"
|
||||
|
||||
# [unexporTed] builds non-exporTed functions, variables and types...
|
||||
# type: bool
|
||||
# default: false
|
||||
unexporTed: false
|
||||
|
||||
# [spread] means it will make a file to hold all fileb0x data
|
||||
# and each file into a separaTed .go file
|
||||
#
|
||||
# example:
|
||||
# theres 2 files in the folder assets, they're: hello.json and world.txt
|
||||
# when spread is activaTed, fileb0x will make a file:
|
||||
# b0x.go or [output]'s data, assets_hello.json.go and assets_world.txt.go
|
||||
#
|
||||
#
|
||||
# type: bool
|
||||
# default: false
|
||||
spread: true
|
||||
|
||||
# [lcf] log changed files when spread is active
|
||||
lcf: true
|
||||
|
||||
# type: array of objects
|
||||
custom:
|
||||
|
||||
# type: array of strings
|
||||
- files:
|
||||
- "assets/css/bulma-0.7.0.min.css"
|
||||
- "assets/css/bulma-tooltip-1.0.4.min.css"
|
||||
- "assets/css/bulma.css.map"
|
||||
- "assets/css/style.css"
|
||||
- "assets/js/fontawesome-5.0.7.js"
|
||||
|
||||
# base is the path that will be removed from all files' path
|
||||
# type: string
|
||||
base: "assets"
|
||||
|
||||
# prefix is the path that will be added to all files' path
|
||||
# type: string
|
||||
prefix: "static/"
|
||||
# end: files
|
||||
- files:
|
||||
- "assets/error.html"
|
||||
- "assets/index.html"
|
||||
- "assets/pagination_ellipsis.html"
|
||||
- "assets/pagination_link_current.html"
|
||||
- "assets/pagination_link.html"
|
||||
- "assets/pagination.html"
|
||||
- "assets/passworded_paste_verify.html"
|
||||
- "assets/paste.html"
|
||||
- "assets/pastelist_list.html"
|
||||
- "assets/pastelist_paste.html"
|
||||
base: "assets"
|
||||
prefix: ""
|
23
go.mod
23
go.mod
|
@ -1,23 +0,0 @@
|
|||
module go.dev.pztrn.name/fastpastebin
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.2.0
|
||||
github.com/dchest/captcha v1.0.0
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/labstack/echo v3.3.10+incompatible
|
||||
github.com/labstack/gommon v0.3.1 // indirect
|
||||
github.com/lib/pq v1.10.6
|
||||
github.com/pressly/goose v2.7.0+incompatible
|
||||
github.com/rs/zerolog v1.27.0
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
|
||||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
91
go.sum
91
go.sum
|
@ -1,91 +0,0 @@
|
|||
github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/captcha v1.0.0 h1:vw+bm/qMFvTgcjQlYVTuQBJkarm5R0YSsDKhm1HZI2o=
|
||||
github.com/dchest/captcha v1.0.0/go.mod h1:7zoElIawLp7GUMLcj54K9kbw+jEyvz2K0FDdRRYhvWo=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
||||
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
|
||||
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
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/pressly/goose v2.7.0+incompatible h1:PWejVEv07LCerQEzMMeAtjuyCKbyprZ/LBa6K5P0OCQ=
|
||||
github.com/pressly/goose v2.7.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
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-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJIkPFVM6oDtMFYmgws0=
|
||||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -1,46 +0,0 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
"go.dev.pztrn.name/fastpastebin/assets"
|
||||
)
|
||||
|
||||
func (c *Context) initializeHTTPServer() {
|
||||
c.Echo = echo.New()
|
||||
c.Echo.Use(c.echoReqLogger())
|
||||
c.Echo.Use(middleware.Recover())
|
||||
c.Echo.Use(middleware.BodyLimit(c.Config.HTTP.MaxBodySizeMegabytes + "M"))
|
||||
c.Echo.DisableHTTP2 = true
|
||||
c.Echo.HideBanner = true
|
||||
c.Echo.HidePort = true
|
||||
|
||||
// Static files.
|
||||
c.Echo.GET("/static/*", echo.WrapHandler(http.FileServer(http.FS(assets.Data))))
|
||||
|
||||
listenAddress := c.Config.HTTP.Address + ":" + c.Config.HTTP.Port
|
||||
|
||||
go func() {
|
||||
c.Echo.Logger.Fatal(c.Echo.Start(listenAddress))
|
||||
}()
|
||||
c.Logger.Info().Str("address", listenAddress).Msg("Started HTTP server")
|
||||
}
|
||||
|
||||
// Wrapper around previous function.
|
||||
func (c *Context) echoReqLogger() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ectx echo.Context) error {
|
||||
c.Logger.Info().
|
||||
Str("IP", ectx.RealIP()).
|
||||
Str("Host", ectx.Request().Host).
|
||||
Str("Method", ectx.Request().Method).
|
||||
Str("Path", ectx.Request().URL.Path).
|
||||
Str("UA", ectx.Request().UserAgent()).
|
||||
Msg("HTTP request")
|
||||
|
||||
return next(ectx)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Puts memory usage into log lines.
|
||||
func (c *Context) getMemoryUsage(event *zerolog.Event, level zerolog.Level, message string) {
|
||||
var memstats runtime.MemStats
|
||||
|
||||
runtime.ReadMemStats(&memstats)
|
||||
|
||||
event.Str("memalloc", fmt.Sprintf("%dMB", memstats.Alloc/1024/1024))
|
||||
event.Str("memsys", fmt.Sprintf("%dMB", memstats.Sys/1024/1024))
|
||||
event.Str("numgc", fmt.Sprintf("%d", memstats.NumGC))
|
||||
}
|
||||
|
||||
// Initializes logger.
|
||||
func (c *Context) initializeLogger() {
|
||||
// Устанавливаем форматирование логгера.
|
||||
//nolint:exhaustruct
|
||||
output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false, TimeFormat: time.RFC3339}
|
||||
output.FormatLevel = func(lvlRaw interface{}) string {
|
||||
var lvl string
|
||||
|
||||
if lvlAsString, ok := lvlRaw.(string); ok {
|
||||
lvlAsString = strings.ToUpper(lvlAsString)
|
||||
switch lvlAsString {
|
||||
case "DEBUG":
|
||||
lvl = fmt.Sprintf("\x1b[30m%-5s\x1b[0m", lvlAsString)
|
||||
case "ERROR":
|
||||
lvl = fmt.Sprintf("\x1b[31m%-5s\x1b[0m", lvlAsString)
|
||||
case "FATAL":
|
||||
lvl = fmt.Sprintf("\x1b[35m%-5s\x1b[0m", lvlAsString)
|
||||
case "INFO":
|
||||
lvl = fmt.Sprintf("\x1b[32m%-5s\x1b[0m", lvlAsString)
|
||||
case "PANIC":
|
||||
lvl = fmt.Sprintf("\x1b[36m%-5s\x1b[0m", lvlAsString)
|
||||
case "WARN":
|
||||
lvl = fmt.Sprintf("\x1b[33m%-5s\x1b[0m", lvlAsString)
|
||||
default:
|
||||
lvl = lvlAsString
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("| %s |", lvl)
|
||||
}
|
||||
|
||||
c.Logger = zerolog.New(output).With().Timestamp().Logger()
|
||||
|
||||
c.Logger = c.Logger.Hook(zerolog.HookFunc(c.getMemoryUsage))
|
||||
}
|
||||
|
||||
// Initialize logger after configuration parse.
|
||||
func (c *Context) initializeLoggerPost() {
|
||||
// Set log level.
|
||||
c.Logger.Info().Msgf("Setting logger level: %s", c.Config.Logging.LogLevel)
|
||||
|
||||
switch c.Config.Logging.LogLevel {
|
||||
case "DEBUG":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "INFO":
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
case "WARN":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "ERROR":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
case "FATAL":
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
case "PANIC":
|
||||
zerolog.SetGlobalLevel(zerolog.PanicLevel)
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
// MySQL driver.
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/flatfiles"
|
||||
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/mysql"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/postgresql"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
)
|
||||
|
||||
// Database represents control structure for database connection.
|
||||
type Database struct {
|
||||
db dialectinterface.Interface
|
||||
}
|
||||
|
||||
// Database cleanup function. Executes once per hour, hardcoded for now and is
|
||||
// a subject of change in future.
|
||||
func (db *Database) cleanup() {
|
||||
for {
|
||||
ctx.Logger.Info().Msg("Starting pastes cleanup procedure...")
|
||||
|
||||
pages := db.db.GetPastesPages()
|
||||
|
||||
var pasteIDsToRemove []int
|
||||
|
||||
for i := 0; i < pages; i++ {
|
||||
pastes, err := db.db.GetPagedPastes(i)
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Int("page", i).Msg("Failed to perform database cleanup")
|
||||
}
|
||||
|
||||
for _, paste := range pastes {
|
||||
if paste.IsExpired() {
|
||||
pasteIDsToRemove = append(pasteIDsToRemove, paste.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, pasteID := range pasteIDsToRemove {
|
||||
err := db.DeletePaste(pasteID)
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Int("paste", pasteID).Msg("Failed to delete paste!")
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Logger.Info().Msg("Pastes cleanup done.")
|
||||
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) DeletePaste(pasteID int) error {
|
||||
//nolint:wrapcheck
|
||||
return db.db.DeletePaste(pasteID)
|
||||
}
|
||||
|
||||
func (db *Database) GetDatabaseConnection() *sql.DB {
|
||||
if db.db != nil {
|
||||
return db.db.GetDatabaseConnection()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) GetPaste(pasteID int) (*structs.Paste, error) {
|
||||
//nolint:wrapcheck
|
||||
return db.db.GetPaste(pasteID)
|
||||
}
|
||||
|
||||
func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) {
|
||||
//nolint:wrapcheck
|
||||
return db.db.GetPagedPastes(page)
|
||||
}
|
||||
|
||||
func (db *Database) GetPastesPages() int {
|
||||
return db.db.GetPastesPages()
|
||||
}
|
||||
|
||||
// Initialize initializes connection to database.
|
||||
func (db *Database) Initialize() {
|
||||
ctx.Logger.Info().Msg("Initializing database connection...")
|
||||
|
||||
if ctx.Config.Database.Type == "mysql" {
|
||||
mysql.New(ctx)
|
||||
} else if ctx.Config.Database.Type == flatfiles.FlatFileDialect {
|
||||
flatfiles.New(ctx)
|
||||
} else if ctx.Config.Database.Type == "postgresql" {
|
||||
postgresql.New(ctx)
|
||||
} else {
|
||||
ctx.Logger.Fatal().Str("type", ctx.Config.Database.Type).Msg("Unknown database type")
|
||||
}
|
||||
|
||||
go db.cleanup()
|
||||
}
|
||||
|
||||
func (db *Database) RegisterDialect(di dialectinterface.Interface) {
|
||||
db.db = di
|
||||
db.db.Initialize()
|
||||
}
|
||||
|
||||
func (db *Database) SavePaste(p *structs.Paste) (int64, error) {
|
||||
//nolint:wrapcheck
|
||||
return db.db.SavePaste(p)
|
||||
}
|
||||
|
||||
func (db *Database) Shutdown() {
|
||||
db.db.Shutdown()
|
||||
}
|
|
@ -1,304 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package flatfiles
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
)
|
||||
|
||||
type FlatFiles struct {
|
||||
writeMutex sync.Mutex
|
||||
path string
|
||||
pastesIndex []Index
|
||||
}
|
||||
|
||||
// DeletePaste deletes paste from disk and index.
|
||||
func (ff *FlatFiles) DeletePaste(pasteID int) error {
|
||||
// Delete from disk.
|
||||
err := os.Remove(filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json"))
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Msg("Failed to delete paste!")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete from index.
|
||||
ff.writeMutex.Lock()
|
||||
defer ff.writeMutex.Unlock()
|
||||
|
||||
pasteIndex := -1
|
||||
|
||||
for idx, paste := range ff.pastesIndex {
|
||||
if paste.ID == pasteID {
|
||||
pasteIndex = idx
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if pasteIndex != -1 {
|
||||
ff.pastesIndex = append(ff.pastesIndex[:pasteIndex], ff.pastesIndex[pasteIndex+1:]...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ff *FlatFiles) GetDatabaseConnection() *sql.DB {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ff *FlatFiles) GetPaste(pasteID int) (*structs.Paste, error) {
|
||||
ff.writeMutex.Lock()
|
||||
pastePath := filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json")
|
||||
ctx.Logger.Debug().Str("path", pastePath).Msg("Trying to load paste data")
|
||||
|
||||
pasteInBytes, err := os.ReadFile(pastePath)
|
||||
if err != nil {
|
||||
ctx.Logger.Debug().Err(err).Msg("Failed to read paste from storage")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.Logger.Debug().Int("paste bytes", len(pasteInBytes)).Msg("Loaded paste")
|
||||
ff.writeMutex.Unlock()
|
||||
|
||||
//nolint:exhaustruct
|
||||
paste := &structs.Paste{}
|
||||
|
||||
err1 := json.Unmarshal(pasteInBytes, paste)
|
||||
if err1 != nil {
|
||||
ctx.Logger.Error().Err(err1).Msgf("Failed to parse paste")
|
||||
|
||||
//nolint:wrapcheck
|
||||
return nil, err1
|
||||
}
|
||||
|
||||
return paste, nil
|
||||
}
|
||||
|
||||
func (ff *FlatFiles) GetPagedPastes(page int) ([]structs.Paste, error) {
|
||||
// Pagination.
|
||||
startPagination := 0
|
||||
if page > 1 {
|
||||
startPagination = (page - 1) * ctx.Config.Pastes.Pagination
|
||||
}
|
||||
|
||||
// Iteration one - get only public pastes.
|
||||
var publicPastes []Index
|
||||
|
||||
for _, paste := range ff.pastesIndex {
|
||||
if !paste.Private {
|
||||
publicPastes = append(publicPastes, paste)
|
||||
}
|
||||
}
|
||||
|
||||
// Iteration two - get paginated pastes.
|
||||
pastesData := make([]structs.Paste, 0)
|
||||
|
||||
for idx, paste := range publicPastes {
|
||||
if len(pastesData) == ctx.Config.Pastes.Pagination {
|
||||
break
|
||||
}
|
||||
|
||||
if idx < startPagination {
|
||||
ctx.Logger.Debug().Int("paste index", idx).Msg("Paste isn't in pagination query: too low index")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (idx-1 >= startPagination && page > 1 && idx > startPagination+((page-1)*ctx.Config.Pastes.Pagination)) || (idx-1 >= startPagination && page == 1 && idx > startPagination+(page*ctx.Config.Pastes.Pagination)) {
|
||||
ctx.Logger.Debug().Int("paste index", idx).Msg("Paste isn't in pagination query: too high index")
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
ctx.Logger.Debug().Int("ID", paste.ID).Int("index", idx).Msg("Getting paste data")
|
||||
|
||||
// Get paste data.
|
||||
//nolint:exhaustruct
|
||||
pasteData := &structs.Paste{}
|
||||
|
||||
pasteRawData, err := os.ReadFile(filepath.Join(ff.path, "pastes", strconv.Itoa(paste.ID)+".json"))
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Msg("Failed to read paste data")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
err1 := json.Unmarshal(pasteRawData, pasteData)
|
||||
if err1 != nil {
|
||||
ctx.Logger.Error().Err(err1).Msg("Failed to parse paste data")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
pastesData = append(pastesData, (*pasteData))
|
||||
}
|
||||
|
||||
return pastesData, nil
|
||||
}
|
||||
|
||||
func (ff *FlatFiles) GetPastesPages() int {
|
||||
// Get public pastes count.
|
||||
var publicPastes []Index
|
||||
|
||||
ff.writeMutex.Lock()
|
||||
for _, paste := range ff.pastesIndex {
|
||||
if !paste.Private {
|
||||
publicPastes = append(publicPastes, paste)
|
||||
}
|
||||
}
|
||||
ff.writeMutex.Unlock()
|
||||
|
||||
// Calculate pages.
|
||||
pages := len(publicPastes) / ctx.Config.Pastes.Pagination
|
||||
// Check if we have any remainder. Add 1 to pages count if so.
|
||||
if len(publicPastes)%ctx.Config.Pastes.Pagination > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
func (ff *FlatFiles) Initialize() {
|
||||
ctx.Logger.Info().Msg("Initializing flatfiles storage...")
|
||||
|
||||
path := ctx.Config.Database.Path
|
||||
// Get proper paste file path.
|
||||
if strings.Contains(ctx.Config.Database.Path, "~") {
|
||||
curUser, err := user.Current()
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Msg("Failed to get current user. Will replace '~' for '/' in storage path!")
|
||||
|
||||
path = strings.Replace(path, "~", "/", -1)
|
||||
}
|
||||
|
||||
path = strings.Replace(path, "~", curUser.HomeDir, -1)
|
||||
}
|
||||
|
||||
path, _ = filepath.Abs(path)
|
||||
ff.path = path
|
||||
|
||||
ctx.Logger.Debug().Msgf("Storage path is now: %s", ff.path)
|
||||
|
||||
// Create directory if necessary.
|
||||
if _, err := os.Stat(ff.path); err != nil {
|
||||
ctx.Logger.Debug().Str("directory", ff.path).Msg("Directory does not exist, creating...")
|
||||
_ = os.MkdirAll(ff.path, os.ModePerm)
|
||||
} else {
|
||||
ctx.Logger.Debug().Str("directory", ff.path).Msg("Directory already exists")
|
||||
}
|
||||
|
||||
// Create directory for pastes.
|
||||
if _, err := os.Stat(filepath.Join(ff.path, "pastes")); err != nil {
|
||||
ctx.Logger.Debug().Str("directory", ff.path).Msg("Directory does not exist, creating...")
|
||||
_ = os.MkdirAll(filepath.Join(ff.path, "pastes"), os.ModePerm)
|
||||
} else {
|
||||
ctx.Logger.Debug().Str("directory", ff.path).Msg("Directory already exists")
|
||||
}
|
||||
|
||||
// Load pastes index.
|
||||
ff.pastesIndex = []Index{}
|
||||
if _, err := os.Stat(filepath.Join(ff.path, "pastes", "index.json")); err != nil {
|
||||
ctx.Logger.Warn().Msg("Pastes index file does not exist, will create new one")
|
||||
} else {
|
||||
indexData, err := os.ReadFile(filepath.Join(ff.path, "pastes", "index.json"))
|
||||
if err != nil {
|
||||
ctx.Logger.Fatal().Msg("Failed to read contents of index file!")
|
||||
}
|
||||
|
||||
err1 := json.Unmarshal(indexData, &ff.pastesIndex)
|
||||
if err1 != nil {
|
||||
ctx.Logger.Error().Err(err1).Msg("Failed to parse index file contents from JSON into internal structure. Will create new index file. All of your previous pastes will became unavailable.")
|
||||
}
|
||||
|
||||
ctx.Logger.Debug().Int("pastes count", len(ff.pastesIndex)).Msg("Parsed pastes index")
|
||||
}
|
||||
}
|
||||
|
||||
func (ff *FlatFiles) SavePaste(paste *structs.Paste) (int64, error) {
|
||||
ff.writeMutex.Lock()
|
||||
// Write paste data on disk.
|
||||
filesOnDisk, _ := os.ReadDir(filepath.Join(ff.path, "pastes"))
|
||||
pasteID := len(filesOnDisk) + 1
|
||||
paste.ID = pasteID
|
||||
|
||||
ctx.Logger.Debug().Int("new paste ID", pasteID).Msg("Writing paste to disk")
|
||||
|
||||
data, err := json.Marshal(paste)
|
||||
if err != nil {
|
||||
ff.writeMutex.Unlock()
|
||||
|
||||
//nolint:wrapcheck
|
||||
return 0, err
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json"), data, 0o600)
|
||||
if err != nil {
|
||||
ff.writeMutex.Unlock()
|
||||
|
||||
//nolint:wrapcheck
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Add it to cache.
|
||||
//nolint:exhaustruct
|
||||
indexData := Index{}
|
||||
indexData.ID = pasteID
|
||||
indexData.Private = paste.Private
|
||||
ff.pastesIndex = append(ff.pastesIndex, indexData)
|
||||
ff.writeMutex.Unlock()
|
||||
|
||||
return int64(pasteID), nil
|
||||
}
|
||||
|
||||
func (ff *FlatFiles) Shutdown() {
|
||||
ctx.Logger.Info().Msg("Saving indexes...")
|
||||
|
||||
indexData, err := json.Marshal(ff.pastesIndex)
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Msg("Failed to encode index data into JSON")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err1 := os.WriteFile(filepath.Join(ff.path, "pastes", "index.json"), indexData, 0o600)
|
||||
if err1 != nil {
|
||||
ctx.Logger.Error().Err(err1).Msg("Failed to write index data to file. Pretty sure that you've lost your pastes.")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package flatfiles
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
)
|
||||
|
||||
type Handler struct{}
|
||||
|
||||
func (dbh Handler) DeletePaste(pasteID int) error {
|
||||
return flf.DeletePaste(pasteID)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
||||
return flf.GetDatabaseConnection()
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
|
||||
return flf.GetPaste(pasteID)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
|
||||
return flf.GetPagedPastes(page)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPastesPages() int {
|
||||
return flf.GetPastesPages()
|
||||
}
|
||||
|
||||
func (dbh Handler) Initialize() {
|
||||
flf.Initialize()
|
||||
}
|
||||
|
||||
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
|
||||
return flf.SavePaste(p)
|
||||
}
|
||||
|
||||
func (dbh Handler) Shutdown() {
|
||||
flf.Shutdown()
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package dialectinterface
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
DeletePaste(int) error
|
||||
GetDatabaseConnection() *sql.DB
|
||||
GetPaste(pasteID int) (*structs.Paste, error)
|
||||
GetPagedPastes(page int) ([]structs.Paste, error)
|
||||
GetPastesPages() int
|
||||
Initialize()
|
||||
SavePaste(p *structs.Paste) (int64, error)
|
||||
Shutdown()
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
)
|
||||
|
||||
type Handler struct{}
|
||||
|
||||
func (dbh Handler) DeletePaste(pasteID int) error {
|
||||
return dbAdapter.DeletePaste(pasteID)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
||||
return dbAdapter.GetDatabaseConnection()
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
|
||||
return dbAdapter.GetPaste(pasteID)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
|
||||
return dbAdapter.GetPagedPastes(page)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPastesPages() int {
|
||||
return dbAdapter.GetPastesPages()
|
||||
}
|
||||
|
||||
func (dbh Handler) Initialize() {
|
||||
dbAdapter.Initialize()
|
||||
}
|
||||
|
||||
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
|
||||
return dbAdapter.SavePaste(p)
|
||||
}
|
||||
|
||||
func (dbh Handler) Shutdown() {
|
||||
dbAdapter.Shutdown()
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
// MySQL driver.
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/mysql/migrations"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
)
|
||||
|
||||
// Database is a MySQL/MariaDB connection controlling structure.
|
||||
type Database struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
// Checks if queries can be run on known connection and reestablish it
|
||||
// if not.
|
||||
func (db *Database) check() {
|
||||
if db.db != nil {
|
||||
_, err := db.db.Exec("SELECT 1")
|
||||
if err != nil {
|
||||
db.Initialize()
|
||||
}
|
||||
} else {
|
||||
db.Initialize()
|
||||
}
|
||||
}
|
||||
|
||||
// DeletePaste deletes paste from database.
|
||||
func (db *Database) DeletePaste(pasteID int) error {
|
||||
db.check()
|
||||
|
||||
_, err := db.db.Exec(db.db.Rebind("DELETE FROM pastes WHERE id=?"), pasteID)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) GetDatabaseConnection() *sql.DB {
|
||||
db.check()
|
||||
|
||||
if db.db != nil {
|
||||
return db.db.DB
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPaste returns a single paste by ID.
|
||||
func (db *Database) GetPaste(pasteID int) (*structs.Paste, error) {
|
||||
db.check()
|
||||
|
||||
//nolint:exhaustruct
|
||||
paste := &structs.Paste{}
|
||||
|
||||
err := db.db.Get(paste, db.db.Rebind("SELECT * FROM `pastes` WHERE id=?"), pasteID)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return paste, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) {
|
||||
db.check()
|
||||
|
||||
var (
|
||||
pastesRaw []structs.Paste
|
||||
pastes []structs.Paste
|
||||
)
|
||||
|
||||
// Pagination.
|
||||
startPagination := 0
|
||||
if page > 1 {
|
||||
startPagination = (page - 1) * ctx.Config.Pastes.Pagination
|
||||
}
|
||||
|
||||
err := db.db.Select(&pastesRaw, db.db.Rebind("SELECT * FROM `pastes` WHERE private != true ORDER BY id DESC LIMIT ? OFFSET ?"), ctx.Config.Pastes.Pagination, startPagination)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range pastesRaw {
|
||||
if !pastesRaw[i].IsExpired() {
|
||||
pastes = append(pastes, pastesRaw[i])
|
||||
}
|
||||
}
|
||||
|
||||
return pastes, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetPastesPages() int {
|
||||
db.check()
|
||||
|
||||
var (
|
||||
pastesRaw []structs.Paste
|
||||
pastes []structs.Paste
|
||||
)
|
||||
|
||||
err := db.db.Get(&pastesRaw, "SELECT * FROM `pastes` WHERE private != true")
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check if pastes isn't expired.
|
||||
for i := range pastesRaw {
|
||||
if !pastesRaw[i].IsExpired() {
|
||||
pastes = append(pastes, pastesRaw[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate pages.
|
||||
pages := len(pastes) / ctx.Config.Pastes.Pagination
|
||||
// Check if we have any remainder. Add 1 to pages count if so.
|
||||
if len(pastes)%ctx.Config.Pastes.Pagination > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// Initialize initializes MySQL/MariaDB connection.
|
||||
func (db *Database) Initialize() {
|
||||
ctx.Logger.Info().Msg("Initializing database connection...")
|
||||
|
||||
// There might be only user, without password. MySQL/MariaDB driver
|
||||
// in DSN wants "user" or "user:password", "user:" is invalid.
|
||||
var userpass string
|
||||
if ctx.Config.Database.Password == "" {
|
||||
userpass = ctx.Config.Database.Username
|
||||
} else {
|
||||
userpass = ctx.Config.Database.Username + ":" + ctx.Config.Database.Password
|
||||
}
|
||||
|
||||
dbConnString := fmt.Sprintf("%s@tcp(%s:%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci&charset=utf8mb4", userpass, ctx.Config.Database.Address, ctx.Config.Database.Port, ctx.Config.Database.Database)
|
||||
ctx.Logger.Debug().Str("DSN", dbConnString).Msgf("Database connection string composed")
|
||||
|
||||
dbConn, err := sqlx.Connect("mysql", dbConnString)
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Msg("Failed to connect to database")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Force UTC for current connection.
|
||||
_ = dbConn.MustExec("SET @@session.time_zone='+00:00';")
|
||||
|
||||
ctx.Logger.Info().Msg("Database connection established")
|
||||
|
||||
db.db = dbConn
|
||||
|
||||
// Perform migrations.
|
||||
migrations.New(ctx)
|
||||
migrations.Migrate()
|
||||
}
|
||||
|
||||
func (db *Database) SavePaste(p *structs.Paste) (int64, error) {
|
||||
db.check()
|
||||
|
||||
result, err := db.db.NamedExec("INSERT INTO `pastes` (title, data, created_at, keep_for, keep_for_unit_type, language, private, password, password_salt) VALUES (:title, :data, :created_at, :keep_for, :keep_for_unit_type, :language, :private, :password, :password_salt)", p)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return 0, err
|
||||
}
|
||||
|
||||
lastInsertID, err1 := result.LastInsertId()
|
||||
if err1 != nil {
|
||||
//nolint:wrapcheck
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return lastInsertID, nil
|
||||
}
|
||||
|
||||
func (db *Database) Shutdown() {
|
||||
if db.db != nil {
|
||||
err := db.db.Close()
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Msg("Failed to close database connection")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
)
|
||||
|
||||
type Handler struct{}
|
||||
|
||||
func (dbh Handler) DeletePaste(pasteID int) error {
|
||||
return dbAdapter.DeletePaste(pasteID)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
||||
return dbAdapter.GetDatabaseConnection()
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
|
||||
return dbAdapter.GetPaste(pasteID)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
|
||||
return dbAdapter.GetPagedPastes(page)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPastesPages() int {
|
||||
return dbAdapter.GetPastesPages()
|
||||
}
|
||||
|
||||
func (dbh Handler) Initialize() {
|
||||
dbAdapter.Initialize()
|
||||
}
|
||||
|
||||
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
|
||||
return dbAdapter.SavePaste(p)
|
||||
}
|
||||
|
||||
func (dbh Handler) Shutdown() {
|
||||
dbAdapter.Shutdown()
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func InitialUp(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE pastes
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
keep_for INTEGER NOT NULL DEFAULT 1,
|
||||
keep_for_unit_type SMALLINT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN pastes.id IS 'Paste ID';
|
||||
COMMENT ON COLUMN pastes.title IS 'Paste title';
|
||||
COMMENT ON COLUMN pastes.data IS 'Paste data';
|
||||
COMMENT ON COLUMN pastes.created_at IS 'Paste creation timestamp';
|
||||
COMMENT ON COLUMN pastes.keep_for IS 'Keep for integer. 0 - forever.';
|
||||
COMMENT ON COLUMN pastes.keep_for_unit_type IS 'Keep for unit type. 0 - forever, 1 - minutes, 2 - hours, 3 - days, 4 - months.';
|
||||
`)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func PasteLangUp(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE pastes ADD COLUMN language VARCHAR(191) NOT NULL DEFAULT 'text'; COMMENT ON COLUMN pastes.language IS 'Paste language';")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PasteLangDown(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE pastes DROP COLUMN language")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func PrivatePastesUp(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE pastes ADD COLUMN private BOOLEAN NOT NULL DEFAULT false; COMMENT ON COLUMN pastes.private IS 'Private paste? If true - additional URL parameter (UNIX TIMESTAMP) of paste will be required to access.';")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PrivatePastesDown(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE pastes DROP COLUMN private")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func PasswordedPastesUp(txn *sql.Tx) error {
|
||||
_, err := txn.Exec("ALTER TABLE pastes ADD COLUMN password VARCHAR(64) NOT NULL DEFAULT ''; COMMENT ON COLUMN pastes.password IS 'Password for paste (scrypted and sha256ed).';")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
_, err1 := txn.Exec("ALTER TABLE pastes ADD COLUMN password_salt VARCHAR(64) NOT NULL DEFAULT ''; COMMENT ON COLUMN pastes.password_salt IS 'Password salt (sha256ed).';")
|
||||
if err1 != nil {
|
||||
//nolint:wrapcheck
|
||||
return err1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PasswordedPastesDown(txn *sql.Tx) error {
|
||||
_, err := txn.Exec("ALTER TABLE pastes DROP COLUMN password")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
_, err1 := txn.Exec("ALTER TABLE pastes DROP COLUMN password_salt")
|
||||
if err1 != nil {
|
||||
//nolint:wrapcheck
|
||||
return err1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pressly/goose"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
)
|
||||
|
||||
var ctx *context.Context
|
||||
|
||||
// New initializes migrations.
|
||||
func New(cc *context.Context) {
|
||||
ctx = cc
|
||||
}
|
||||
|
||||
// Migrate launching migrations.
|
||||
func Migrate() {
|
||||
ctx.Logger.Info().Msg("Migrating database...")
|
||||
|
||||
_ = goose.SetDialect("postgres")
|
||||
goose.AddNamedMigration("1_initial.go", InitialUp, nil)
|
||||
goose.AddNamedMigration("2_paste_lang.go", PasteLangUp, PasteLangDown)
|
||||
goose.AddNamedMigration("3_private_pastes.go", PrivatePastesUp, PrivatePastesDown)
|
||||
goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown)
|
||||
// Add new migrations BEFORE this message.
|
||||
|
||||
dbConn := ctx.Database.GetDatabaseConnection()
|
||||
if dbConn != nil {
|
||||
err := goose.Up(dbConn, ".")
|
||||
if err != nil {
|
||||
ctx.Logger.Info().Msgf("%+v", err)
|
||||
ctx.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
ctx.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping")
|
||||
}
|
||||
}
|
|
@ -1,228 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package postgresql
|
||||
|
||||
//nolint:gci
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
// PostgreSQL driver.
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/postgresql/migrations"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
)
|
||||
|
||||
// Database is a PostgreSQL connection controlling structure.
|
||||
type Database struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
// Checks if queries can be run on known connection and reestablish it
|
||||
// if not.
|
||||
func (db *Database) check() {
|
||||
if db.db != nil {
|
||||
_, err := db.db.Exec("SELECT 1")
|
||||
if err != nil {
|
||||
db.Initialize()
|
||||
}
|
||||
} else {
|
||||
db.Initialize()
|
||||
}
|
||||
}
|
||||
|
||||
// DeletePaste deletes paste from database.
|
||||
func (db *Database) DeletePaste(pasteID int) error {
|
||||
db.check()
|
||||
|
||||
_, err := db.db.Exec(db.db.Rebind("DELETE FROM pastes WHERE id=?"), pasteID)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) GetDatabaseConnection() *sql.DB {
|
||||
db.check()
|
||||
|
||||
if db.db != nil {
|
||||
return db.db.DB
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPaste returns a single paste by ID.
|
||||
func (db *Database) GetPaste(pasteID int) (*structs.Paste, error) {
|
||||
db.check()
|
||||
|
||||
//nolint:exhaustruct
|
||||
paste := &structs.Paste{}
|
||||
|
||||
err := db.db.Get(paste, db.db.Rebind("SELECT * FROM pastes WHERE id=$1"), pasteID)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We're aware of timezone in PostgreSQL, so SELECT will return
|
||||
// timestamps in server's local timezone. We should convert them.
|
||||
loc, _ := time.LoadLocation("UTC")
|
||||
|
||||
utcCreatedAt := paste.CreatedAt.In(loc)
|
||||
paste.CreatedAt = &utcCreatedAt
|
||||
|
||||
return paste, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) {
|
||||
db.check()
|
||||
|
||||
var (
|
||||
pastesRaw []structs.Paste
|
||||
pastes []structs.Paste
|
||||
)
|
||||
|
||||
// Pagination.
|
||||
startPagination := 0
|
||||
if page > 1 {
|
||||
startPagination = (page - 1) * ctx.Config.Pastes.Pagination
|
||||
}
|
||||
|
||||
err := db.db.Select(&pastesRaw, db.db.Rebind("SELECT * FROM pastes WHERE private != true ORDER BY id DESC LIMIT $1 OFFSET $2"), ctx.Config.Pastes.Pagination, startPagination)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We're aware of timezone in PostgreSQL, so SELECT will return
|
||||
// timestamps in server's local timezone. We should convert them.
|
||||
loc, _ := time.LoadLocation("UTC")
|
||||
|
||||
for _, paste := range pastesRaw {
|
||||
if !paste.IsExpired() {
|
||||
utcCreatedAt := paste.CreatedAt.In(loc)
|
||||
paste.CreatedAt = &utcCreatedAt
|
||||
pastes = append(pastes, paste)
|
||||
}
|
||||
}
|
||||
|
||||
return pastes, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetPastesPages() int {
|
||||
db.check()
|
||||
|
||||
var (
|
||||
pastesRaw []structs.Paste
|
||||
pastes []structs.Paste
|
||||
)
|
||||
|
||||
err := db.db.Get(&pastesRaw, "SELECT * FROM pastes WHERE private != true")
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check if pastes isn't expired.
|
||||
for _, paste := range pastesRaw {
|
||||
if !paste.IsExpired() {
|
||||
pastes = append(pastes, paste)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate pages.
|
||||
pages := len(pastes) / ctx.Config.Pastes.Pagination
|
||||
// Check if we have any remainder. Add 1 to pages count if so.
|
||||
if len(pastes)%ctx.Config.Pastes.Pagination > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// Initialize initializes MySQL/MariaDB connection.
|
||||
func (db *Database) Initialize() {
|
||||
ctx.Logger.Info().Msg("Initializing database connection...")
|
||||
|
||||
var userpass string
|
||||
if ctx.Config.Database.Password == "" {
|
||||
userpass = ctx.Config.Database.Username
|
||||
} else {
|
||||
userpass = ctx.Config.Database.Username + ":" + ctx.Config.Database.Password
|
||||
}
|
||||
|
||||
dbConnString := fmt.Sprintf("postgres://%s@%s/%s?connect_timeout=10&fallback_application_name=fastpastebin&sslmode=disable", userpass, net.JoinHostPort(ctx.Config.Database.Address, ctx.Config.Database.Port), ctx.Config.Database.Database)
|
||||
ctx.Logger.Debug().Str("DSN", dbConnString).Msg("Database connection string composed")
|
||||
|
||||
dbConn, err := sqlx.Connect("postgres", dbConnString)
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Msg("Failed to connect to database")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Logger.Info().Msg("Database connection established")
|
||||
|
||||
db.db = dbConn
|
||||
|
||||
// Perform migrations.
|
||||
migrations.New(ctx)
|
||||
migrations.Migrate()
|
||||
}
|
||||
|
||||
func (db *Database) SavePaste(paste *structs.Paste) (int64, error) {
|
||||
db.check()
|
||||
|
||||
stmt, err := db.db.PrepareNamed("INSERT INTO pastes (title, data, created_at, keep_for, keep_for_unit_type, language, private, password, password_salt) VALUES (:title, :data, :created_at, :keep_for, :keep_for_unit_type, :language, :private, :password, :password_salt) RETURNING id")
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var newPasteID int64
|
||||
|
||||
err = stmt.Get(&newPasteID, paste)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newPasteID, nil
|
||||
}
|
||||
|
||||
func (db *Database) Shutdown() {
|
||||
if db.db != nil {
|
||||
err := db.db.Close()
|
||||
if err != nil {
|
||||
ctx.Logger.Error().Err(err).Msg("Failed to close database connection")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
||||
)
|
||||
|
||||
// Handler is an interfaceable structure that proxifies calls from anyone
|
||||
// to Database structure.
|
||||
type Handler struct{}
|
||||
|
||||
func (dbh Handler) DeletePaste(pasteID int) error {
|
||||
return dbAdapter.DeletePaste(pasteID)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
||||
return dbAdapter.GetDatabaseConnection()
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
|
||||
return dbAdapter.GetPaste(pasteID)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
|
||||
return dbAdapter.GetPagedPastes(page)
|
||||
}
|
||||
|
||||
func (dbh Handler) GetPastesPages() int {
|
||||
return dbAdapter.GetPastesPages()
|
||||
}
|
||||
|
||||
// Initialize initializes connection to database.
|
||||
func (dbh Handler) Initialize() {
|
||||
dbAdapter.Initialize()
|
||||
}
|
||||
|
||||
func (dbh Handler) RegisterDialect(di dialectinterface.Interface) {
|
||||
dbAdapter.RegisterDialect(di)
|
||||
}
|
||||
|
||||
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
|
||||
return dbAdapter.SavePaste(p)
|
||||
}
|
||||
|
||||
func (dbh Handler) Shutdown() {
|
||||
dbAdapter.Shutdown()
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package templater
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/rs/zerolog"
|
||||
"go.dev.pztrn.name/fastpastebin/assets"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *context.Context
|
||||
log zerolog.Logger
|
||||
)
|
||||
|
||||
// GetErrorTemplate returns formatted error template.
|
||||
// If error.html wasn't found - it will return "error.html not found"
|
||||
// message as simple string.
|
||||
func GetErrorTemplate(ec echo.Context, errorText string) string {
|
||||
// Getting main error template.
|
||||
mainhtml := GetTemplate(ec, "error.html", map[string]string{"error": errorText})
|
||||
|
||||
return mainhtml
|
||||
}
|
||||
|
||||
// GetRawTemplate returns only raw template data.
|
||||
func GetRawTemplate(ectx echo.Context, templateName string, data map[string]string) string {
|
||||
// Getting main template.
|
||||
tplRaw, err := assets.Data.ReadFile(templateName)
|
||||
if err != nil {
|
||||
_ = ectx.String(http.StatusBadRequest, templateName+" not found.")
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
tpl := string(tplRaw)
|
||||
// Replace placeholders with data from data map.
|
||||
for placeholder, value := range data {
|
||||
tpl = strings.Replace(tpl, "{"+placeholder+"}", value, -1)
|
||||
}
|
||||
|
||||
return tpl
|
||||
}
|
||||
|
||||
// GetTemplate returns formatted template that can be outputted to client.
|
||||
func GetTemplate(ectx echo.Context, name string, data map[string]string) string {
|
||||
log.Debug().Str("name", name).Msg("Requested template")
|
||||
|
||||
// Getting main template.
|
||||
mainhtml, err := assets.Data.ReadFile("main.html")
|
||||
if err != nil {
|
||||
_ = ectx.String(http.StatusBadRequest, "main.html not found.")
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Getting navigation.
|
||||
navhtml, err1 := assets.Data.ReadFile("navigation.html")
|
||||
if err1 != nil {
|
||||
_ = ectx.String(http.StatusBadRequest, "navigation.html not found.")
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Getting footer.
|
||||
footerhtml, err2 := assets.Data.ReadFile("footer.html")
|
||||
if err2 != nil {
|
||||
_ = ectx.String(http.StatusBadRequest, "footer.html not found.")
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Format main template.
|
||||
tpl := strings.Replace(string(mainhtml), "{navigation}", string(navhtml), 1)
|
||||
tpl = strings.Replace(tpl, "{footer}", string(footerhtml), 1)
|
||||
// Version.
|
||||
tpl = strings.Replace(tpl, "{version}", context.Version, 1)
|
||||
|
||||
// Get requested template.
|
||||
reqhtml, err3 := assets.Data.ReadFile(name)
|
||||
if err3 != nil {
|
||||
_ = ectx.String(http.StatusBadRequest, name+" not found.")
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Replace documentBody.
|
||||
tpl = strings.Replace(tpl, "{documentBody}", string(reqhtml), 1)
|
||||
|
||||
// Replace placeholders with data from data map.
|
||||
for placeholder, value := range data {
|
||||
tpl = strings.Replace(tpl, "{"+placeholder+"}", value, -1)
|
||||
}
|
||||
|
||||
return tpl
|
||||
}
|
||||
|
||||
// Initialize initializes package.
|
||||
func Initialize(cc *context.Context) {
|
||||
ctx = cc
|
||||
log = ctx.Logger.With().Str("type", "internal").Str("package", "templater").Logger()
|
||||
}
|
|
@ -1,61 +1,56 @@
|
|||
package pagination
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.dev.pztrn.name/fastpastebin/assets"
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/api/http/static"
|
||||
)
|
||||
|
||||
// CreateHTML creates pagination HTML based on passed parameters.
|
||||
func CreateHTML(currentPage int, pages int, linksBase string) string {
|
||||
// Load templates.
|
||||
paginationHTMLRaw, err := assets.Data.ReadFile("pagination.html")
|
||||
paginationHTMLRaw, err := static.ReadFile("pagination.html")
|
||||
if err != nil {
|
||||
return "Missing pagination.html"
|
||||
}
|
||||
|
||||
paginationLinkRaw, err1 := assets.Data.ReadFile("pagination_link.html")
|
||||
paginationLinkRaw, err1 := static.ReadFile("pagination_link.html")
|
||||
if err1 != nil {
|
||||
return "Missing pagination_link.html"
|
||||
}
|
||||
|
||||
paginationLinkCurrentRaw, err2 := assets.Data.ReadFile("pagination_link_current.html")
|
||||
paginationLinkCurrentRaw, err2 := static.ReadFile("pagination_link_current.html")
|
||||
if err2 != nil {
|
||||
return "Missing pagination_link_current.html"
|
||||
}
|
||||
|
||||
paginationEllipsisRaw, err3 := assets.Data.ReadFile("pagination_ellipsis.html")
|
||||
paginationEllipsisRaw, err3 := static.ReadFile("pagination_ellipsis.html")
|
||||
if err3 != nil {
|
||||
return "Missing pagination_ellipsis.html"
|
||||
}
|
||||
|
||||
// First page should always be visible.
|
||||
var paginationString string
|
||||
var paginationString = ""
|
||||
if currentPage == 1 {
|
||||
paginationString = strings.Replace(string(paginationLinkCurrentRaw), "{pageNum}", strconv.Itoa(currentPage), -1)
|
||||
} else {
|
||||
paginationString = strings.Replace(string(paginationLinkRaw), "{pageNum}", "1", -1)
|
||||
paginationString = strings.Replace(paginationString, "{paginationLink}", linksBase+"1", -1)
|
||||
paginationString = strings.Replace(string(paginationString), "{paginationLink}", linksBase+"1", -1)
|
||||
}
|
||||
|
||||
var (
|
||||
ellipsisStartAdded = false
|
||||
ellipsisEndAdded = false
|
||||
//nolint:varnamelen
|
||||
i = 2
|
||||
)
|
||||
|
||||
var ellipsisStartAdded = false
|
||||
var ellipsisEndAdded = false
|
||||
i := 2
|
||||
for i <= pages {
|
||||
// ToDo: fix it!
|
||||
//nolint:nestif
|
||||
if pages > 5 {
|
||||
if currentPage-3 < i && currentPage+3 > i || i == pages {
|
||||
paginationItemRaw := string(paginationLinkRaw)
|
||||
var paginationItemRaw = string(paginationLinkRaw)
|
||||
if i == currentPage {
|
||||
paginationItemRaw = string(paginationLinkCurrentRaw)
|
||||
}
|
||||
|
||||
paginationItem := strings.Replace(paginationItemRaw, "{pageNum}", strconv.Itoa(i), -1)
|
||||
paginationItem = strings.Replace(paginationItem, "{paginationLink}", linksBase+strconv.Itoa(i), 1)
|
||||
paginationString += paginationItem
|
||||
|
@ -69,7 +64,7 @@ func CreateHTML(currentPage int, pages int, linksBase string) string {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
paginationItemRaw := string(paginationLinkRaw)
|
||||
var paginationItemRaw = string(paginationLinkRaw)
|
||||
if i == currentPage {
|
||||
paginationItemRaw = string(paginationLinkCurrentRaw)
|
||||
}
|
||||
|
@ -78,7 +73,7 @@ func CreateHTML(currentPage int, pages int, linksBase string) string {
|
|||
paginationString += paginationItem
|
||||
}
|
||||
|
||||
i++
|
||||
i += 1
|
||||
}
|
||||
|
||||
pagination := strings.Replace(string(paginationHTMLRaw), "{paginationLinks}", paginationString, 1)
|
|
@ -0,0 +1,479 @@
|
|||
// Fast Paste Bin - uberfast and easy-to-use pastebin.
|
||||
//
|
||||
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
|
||||
// developers.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject
|
||||
// to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package pastes
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"bytes"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/api/http/static"
|
||||
"github.com/pztrn/fastpastebin/captcha"
|
||||
"github.com/pztrn/fastpastebin/pagination"
|
||||
|
||||
// other
|
||||
"github.com/alecthomas/chroma"
|
||||
"github.com/alecthomas/chroma/formatters"
|
||||
htmlfmt "github.com/alecthomas/chroma/formatters/html"
|
||||
"github.com/alecthomas/chroma/lexers"
|
||||
"github.com/alecthomas/chroma/styles"
|
||||
//"github.com/dchest/captcha"
|
||||
"github.com/labstack/echo"
|
||||
)
|
||||
|
||||
var (
|
||||
regexInts = regexp.MustCompile("[0-9]+")
|
||||
)
|
||||
|
||||
// GET for "/paste/PASTE_ID" and "/paste/PASTE_ID/TIMESTAMP" (private pastes).
|
||||
func pasteGET(ec echo.Context) error {
|
||||
errhtml, err := static.ReadFile("error.html")
|
||||
if err != nil {
|
||||
return ec.String(http.StatusNotFound, "error.html wasn't found!")
|
||||
}
|
||||
|
||||
pasteIDRaw := ec.Param("id")
|
||||
// We already get numbers from string, so we will not check strconv.Atoi()
|
||||
// error.
|
||||
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
||||
c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
|
||||
|
||||
// Get paste.
|
||||
paste, err1 := GetByID(pasteID)
|
||||
if err1 != nil {
|
||||
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
|
||||
if paste.IsExpired() {
|
||||
c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
|
||||
// Check if we have a private paste and it's parameters are correct.
|
||||
if paste.Private {
|
||||
tsProvidedStr := ec.Param("timestamp")
|
||||
tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64)
|
||||
if err2 != nil {
|
||||
c.Logger.Error().Msgf("Invalid timestamp '%s' provided for getting private paste #%d: %s", tsProvidedStr, pasteID, err2.Error())
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
pasteTs := paste.CreatedAt.Unix()
|
||||
if tsProvided != pasteTs {
|
||||
c.Logger.Error().Msgf("Incorrect timestamp '%v' provided for private paste #%d, waiting for %v", tsProvidedStr, pasteID, strconv.FormatInt(pasteTs, 10))
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
}
|
||||
|
||||
if paste.Private && paste.Password != "" {
|
||||
// Check if cookie for this paste is defined. This means that user
|
||||
// previously successfully entered a password.
|
||||
cookie, err := ec.Cookie("PASTE-" + strconv.Itoa(pasteID))
|
||||
if err != nil {
|
||||
// No cookie, redirect to auth page.
|
||||
c.Logger.Info().Msg("Tried to access passworded paste without autorization, redirecting to auth page...")
|
||||
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp")+"/verify")
|
||||
}
|
||||
|
||||
// Generate cookie value to check.
|
||||
cookieValue := paste.GenerateCryptedCookieValue()
|
||||
|
||||
if cookieValue != cookie.Value {
|
||||
c.Logger.Info().Msg("Invalid cookie, redirecting to auth page...")
|
||||
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp")+"/verify")
|
||||
}
|
||||
|
||||
// If all okay - do nothing :)
|
||||
}
|
||||
|
||||
pasteHTML, err2 := static.ReadFile("paste.html")
|
||||
if err2 != nil {
|
||||
return ec.String(http.StatusNotFound, "parse.html wasn't found!")
|
||||
}
|
||||
|
||||
// Format template with paste data.
|
||||
pasteHTMLAsString := strings.Replace(string(pasteHTML), "{pasteTitle}", paste.Title, 1)
|
||||
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteID}", strconv.Itoa(paste.ID), -1)
|
||||
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteDate}", paste.CreatedAt.Format("2006-01-02 @ 15:04:05"), 1)
|
||||
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteExpiration}", paste.GetExpirationTime().Format("2006-01-02 @ 15:04:05"), 1)
|
||||
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteLanguage}", paste.Language, 1)
|
||||
|
||||
if paste.Private {
|
||||
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteType}", "<span class='has-text-danger'>Private</span>", 1)
|
||||
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteTs}", strconv.FormatInt(paste.CreatedAt.Unix(), 10)+"/", 1)
|
||||
} else {
|
||||
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteType}", "<span class='has-text-success'>Public</span>", 1)
|
||||
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteTs}", "", 1)
|
||||
}
|
||||
|
||||
// Highlight.
|
||||
// Get lexer.
|
||||
lexer := lexers.Get(paste.Language)
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
// Tokenize paste data.
|
||||
lexered, err3 := lexer.Tokenise(nil, paste.Data)
|
||||
if err3 != nil {
|
||||
c.Logger.Error().Msgf("Failed to tokenize paste data: %s", err3.Error())
|
||||
}
|
||||
// Get style for HTML output.
|
||||
style := styles.Get("monokai")
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
// Get HTML formatter.
|
||||
formatter := chroma.Formatter(htmlfmt.New(htmlfmt.WithLineNumbers(), htmlfmt.LineNumbersInTable()))
|
||||
if formatter == nil {
|
||||
formatter = formatters.Fallback
|
||||
}
|
||||
// Create buffer and format into it.
|
||||
buf := new(bytes.Buffer)
|
||||
err4 := formatter.Format(buf, style, lexered)
|
||||
if err4 != nil {
|
||||
c.Logger.Error().Msgf("Failed to format paste data: %s", err4.Error())
|
||||
}
|
||||
|
||||
// Escape paste data.
|
||||
//pasteData := html.EscapeString(buf.String())
|
||||
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pastedata}", buf.String(), 1)
|
||||
|
||||
return ec.HTML(http.StatusOK, string(pasteHTMLAsString))
|
||||
}
|
||||
|
||||
// GET for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
|
||||
func pastePasswordedVerifyGet(ec echo.Context) error {
|
||||
verifyHTMLRaw, err := static.ReadFile("passworded_paste_verify.html")
|
||||
if err != nil {
|
||||
return ec.String(http.StatusNotFound, "passworded_paste_verify.html wasn't found!")
|
||||
}
|
||||
|
||||
errhtml, err := static.ReadFile("error.html")
|
||||
if err != nil {
|
||||
return ec.String(http.StatusNotFound, "error.html wasn't found!")
|
||||
}
|
||||
|
||||
pasteIDRaw := ec.Param("id")
|
||||
timestampRaw := ec.Param("timestamp")
|
||||
// We already get numbers from string, so we will not check strconv.Atoi()
|
||||
// error.
|
||||
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
||||
|
||||
// Get paste.
|
||||
paste, err1 := GetByID(pasteID)
|
||||
if err1 != nil {
|
||||
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
|
||||
// Check for auth cookie. If present - redirect to paste.
|
||||
cookie, err := ec.Cookie("PASTE-" + strconv.Itoa(pasteID))
|
||||
if err == nil {
|
||||
// No cookie, redirect to auth page.
|
||||
c.Logger.Debug().Msg("Paste cookie found, checking it...")
|
||||
|
||||
// Generate cookie value to check.
|
||||
cookieValue := paste.GenerateCryptedCookieValue()
|
||||
|
||||
if cookieValue == cookie.Value {
|
||||
c.Logger.Info().Msg("Valid cookie, redirecting to paste page...")
|
||||
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp"))
|
||||
}
|
||||
|
||||
c.Logger.Debug().Msg("Invalid cookie, showing auth page")
|
||||
}
|
||||
|
||||
verifyHTML := strings.Replace(string(verifyHTMLRaw), "{pasteID}", strconv.Itoa(pasteID), -1)
|
||||
verifyHTML = strings.Replace(verifyHTML, "{pasteTimestamp}", timestampRaw, 1)
|
||||
|
||||
return ec.HTML(http.StatusOK, verifyHTML)
|
||||
}
|
||||
|
||||
// POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
|
||||
func pastePasswordedVerifyPost(ec echo.Context) error {
|
||||
errhtml, err := static.ReadFile("error.html")
|
||||
if err != nil {
|
||||
return ec.String(http.StatusNotFound, "error.html wasn't found!")
|
||||
}
|
||||
|
||||
pasteIDRaw := ec.Param("id")
|
||||
timestampRaw := ec.Param("timestamp")
|
||||
// We already get numbers from string, so we will not check strconv.Atoi()
|
||||
// error.
|
||||
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
||||
c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
|
||||
|
||||
// Get paste.
|
||||
paste, err1 := GetByID(pasteID)
|
||||
if err1 != nil {
|
||||
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
|
||||
params, err2 := ec.FormParams()
|
||||
if err2 != nil {
|
||||
c.Logger.Debug().Msg("No form parameters passed")
|
||||
return ec.HTML(http.StatusBadRequest, string(errhtml))
|
||||
}
|
||||
|
||||
if paste.VerifyPassword(params["paste-password"][0]) {
|
||||
// Set cookie that this paste's password is verified and paste
|
||||
// can be viewed.
|
||||
cookie := new(http.Cookie)
|
||||
cookie.Name = "PASTE-" + strconv.Itoa(pasteID)
|
||||
cookie.Value = paste.GenerateCryptedCookieValue()
|
||||
cookie.Expires = time.Now().Add(24 * time.Hour)
|
||||
ec.SetCookie(cookie)
|
||||
|
||||
return ec.Redirect(http.StatusFound, "/paste/"+strconv.Itoa(pasteID)+"/"+timestampRaw)
|
||||
}
|
||||
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Invalid password. Please, try again.", 1)
|
||||
return ec.HTML(http.StatusBadRequest, string(errhtmlAsString))
|
||||
}
|
||||
|
||||
// POST for "/paste/" which will create new paste and redirect to
|
||||
// "/pastes/CREATED_PASTE_ID".
|
||||
func pastePOST(ec echo.Context) error {
|
||||
errhtml, err := static.ReadFile("error.html")
|
||||
if err != nil {
|
||||
return ec.String(http.StatusNotFound, "error.html wasn't found!")
|
||||
}
|
||||
|
||||
params, err := ec.FormParams()
|
||||
if err != nil {
|
||||
c.Logger.Debug().Msg("No form parameters passed")
|
||||
return ec.HTML(http.StatusBadRequest, string(errhtml))
|
||||
}
|
||||
c.Logger.Debug().Msgf("Received parameters: %+v", params)
|
||||
|
||||
// Do nothing if paste contents is empty.
|
||||
if len(params["paste-contents"][0]) == 0 {
|
||||
c.Logger.Debug().Msg("Empty paste submitted, ignoring")
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Empty pastes aren't allowed.", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
|
||||
if !strings.ContainsAny(params["paste-keep-for"][0], "Mmhd") {
|
||||
c.Logger.Debug().Msgf("'Keep paste for' field have invalid value: %s", params["paste-keep-for"][0])
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
|
||||
// Verify captcha.
|
||||
if !captcha.Verify(params["paste-captcha-id"][0], params["paste-captcha-solution"][0]) {
|
||||
c.Logger.Debug().Msgf("Invalid captcha solution for captcha ID '%s': %s", params["paste-captcha-id"][0], params["paste-captcha-solution"][0])
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Invalid captcha solution.", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
|
||||
paste := &Paste{
|
||||
Title: params["paste-title"][0],
|
||||
Data: params["paste-contents"][0],
|
||||
Language: params["paste-language"][0],
|
||||
}
|
||||
|
||||
// Paste creation time in UTC.
|
||||
createdAt := time.Now().UTC()
|
||||
paste.CreatedAt = &createdAt
|
||||
|
||||
// Parse "keep for" field.
|
||||
|
||||
// Get integers and strings separately.
|
||||
keepForUnitRegex := regexp.MustCompile("[Mmhd]")
|
||||
|
||||
keepForRaw := regexInts.FindAllString(params["paste-keep-for"][0], 1)[0]
|
||||
keepFor, err1 := strconv.Atoi(keepForRaw)
|
||||
if err1 != nil {
|
||||
c.Logger.Debug().Msgf("Failed to parse 'Keep for' integer: %s", err1.Error())
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
paste.KeepFor = keepFor
|
||||
|
||||
keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0]
|
||||
keepForUnit := PASTE_KEEPS_CORELLATION[keepForUnitRaw]
|
||||
paste.KeepForUnitType = keepForUnit
|
||||
|
||||
// Try to autodetect if it was selected.
|
||||
if params["paste-language"][0] == "autodetect" {
|
||||
lexer := lexers.Analyse(params["paste-language"][0])
|
||||
if lexer != nil {
|
||||
paste.Language = lexer.Config().Name
|
||||
} else {
|
||||
paste.Language = "text"
|
||||
}
|
||||
}
|
||||
|
||||
// Private paste?
|
||||
paste.Private = false
|
||||
privateCheckbox, privateCheckboxFound := params["paste-private"]
|
||||
pastePassword, pastePasswordFound := params["paste-password"]
|
||||
if privateCheckboxFound && privateCheckbox[0] == "on" || pastePasswordFound && len(pastePassword[0]) != 0 {
|
||||
paste.Private = true
|
||||
}
|
||||
|
||||
if len(pastePassword) != 0 {
|
||||
paste.CreatePassword(pastePassword[0])
|
||||
}
|
||||
|
||||
id, err2 := Save(paste)
|
||||
if err2 != nil {
|
||||
c.Logger.Debug().Msgf("Failed to save paste: %s", err2.Error())
|
||||
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Failed to save paste. Please, try again later.", 1)
|
||||
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
|
||||
}
|
||||
|
||||
newPasteIDAsString := strconv.FormatInt(id, 10)
|
||||
c.Logger.Debug().Msgf("Paste saved, URL: /paste/" + newPasteIDAsString)
|
||||
|
||||
// Private pastes have it's timestamp in URL.
|
||||
if paste.Private {
|
||||
return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString+"/"+strconv.FormatInt(paste.CreatedAt.Unix(), 10))
|
||||
}
|
||||
|
||||
return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString)
|
||||
}
|
||||
|
||||
// GET for "/pastes/:id/raw", raw paste output.
|
||||
func pasteRawGET(ec echo.Context) error {
|
||||
pasteIDRaw := ec.Param("id")
|
||||
// We already get numbers from string, so we will not check strconv.Atoi()
|
||||
// error.
|
||||
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
|
||||
c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
|
||||
|
||||
// Get paste.
|
||||
paste, err1 := GetByID(pasteID)
|
||||
if err1 != nil {
|
||||
c.Logger.Error().Msgf("Failed to get paste #%d from database: %s", pasteID, err1.Error())
|
||||
return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
|
||||
}
|
||||
|
||||
if paste.IsExpired() {
|
||||
c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
|
||||
return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
|
||||
}
|
||||
|
||||
// Check if we have a private paste and it's parameters are correct.
|
||||
if paste.Private {
|
||||
tsProvidedStr := ec.Param("timestamp")
|
||||
tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64)
|
||||
if err2 != nil {
|
||||
c.Logger.Error().Msgf("Invalid timestamp '%s' provided for getting private paste #%d: %s", tsProvidedStr, pasteID, err2.Error())
|
||||
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
|
||||
}
|
||||
pasteTs := paste.CreatedAt.Unix()
|
||||
if tsProvided != pasteTs {
|
||||
c.Logger.Error().Msgf("Incorrect timestamp '%v' provided for private paste #%d, waiting for %v", tsProvidedStr, pasteID, strconv.FormatInt(pasteTs, 10))
|
||||
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
|
||||
}
|
||||
}
|
||||
|
||||
// ToDo: figure out how to handle passworded pastes here.
|
||||
// Return error for now.
|
||||
if paste.Password != "" {
|
||||
c.Logger.Error().Msgf("Cannot render paste #%d as raw: passworded paste. Patches welcome!", pasteID)
|
||||
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
|
||||
}
|
||||
|
||||
return ec.String(http.StatusOK, paste.Data)
|
||||
}
|
||||
|
||||
// GET for "/pastes/", a list of publicly available pastes.
|
||||
func pastesGET(ec echo.Context) error {
|
||||
pasteListHTML, err1 := static.ReadFile("pastelist_list.html")
|
||||
if err1 != nil {
|
||||
return ec.String(http.StatusNotFound, "pastelist_list.html wasn't found!")
|
||||
}
|
||||
|
||||
pasteElementHTML, err2 := static.ReadFile("pastelist_paste.html")
|
||||
if err2 != nil {
|
||||
return ec.String(http.StatusNotFound, "pastelist_paste.html wasn't found!")
|
||||
}
|
||||
|
||||
pageFromParamRaw := ec.Param("page")
|
||||
var page = 1
|
||||
if pageFromParamRaw != "" {
|
||||
pageRaw := regexInts.FindAllString(pageFromParamRaw, 1)[0]
|
||||
page, _ = strconv.Atoi(pageRaw)
|
||||
}
|
||||
|
||||
c.Logger.Debug().Msgf("Requested page #%d", page)
|
||||
|
||||
// Get pastes IDs.
|
||||
pastes, err3 := GetPagedPastes(page)
|
||||
c.Logger.Debug().Msgf("Got %d pastes", len(pastes))
|
||||
|
||||
var pastesString = "No pastes to show."
|
||||
|
||||
// Show "No pastes to show" on any error for now.
|
||||
if err3 != nil {
|
||||
c.Logger.Error().Msgf("Failed to get pastes list from database: %s", err3.Error())
|
||||
pasteListHTMLAsString := strings.Replace(string(pasteListHTML), "{pastes}", pastesString, 1)
|
||||
return ec.HTML(http.StatusOK, string(pasteListHTMLAsString))
|
||||
}
|
||||
|
||||
if len(pastes) > 0 {
|
||||
pastesString = ""
|
||||
for i := range pastes {
|
||||
pasteString := strings.Replace(string(pasteElementHTML), "{pasteID}", strconv.Itoa(pastes[i].ID), 2)
|
||||
pasteString = strings.Replace(pasteString, "{pasteTitle}", pastes[i].Title, 1)
|
||||
pasteString = strings.Replace(pasteString, "{pasteDate}", pastes[i].CreatedAt.Format("2006-01-02 @ 15:04:05"), 1)
|
||||
|
||||
// Get max 4 lines of each paste.
|
||||
pasteDataSplitted := strings.Split(pastes[i].Data, "\n")
|
||||
var pasteData = ""
|
||||
if len(pasteDataSplitted) < 4 {
|
||||
pasteData = pastes[i].Data
|
||||
} else {
|
||||
pasteData = strings.Join(pasteDataSplitted[0:4], "\n")
|
||||
}
|
||||
pasteString = strings.Replace(pasteString, "{pasteData}", pasteData, 1)
|
||||
|
||||
pastesString += pasteString
|
||||
}
|
||||
}
|
||||
|
||||
pasteListHTMLAsString := strings.Replace(string(pasteListHTML), "{pastes}", pastesString, 1)
|
||||
|
||||
pages := GetPastesPages()
|
||||
c.Logger.Debug().Msgf("Total pages: %d, current: %d", pages, page)
|
||||
paginationHTML := pagination.CreateHTML(page, pages, "/pastes/")
|
||||
pasteListHTMLAsString = strings.Replace(pasteListHTMLAsString, "{pagination}", paginationHTML, -1)
|
||||
|
||||
return ec.HTML(http.StatusOK, string(pasteListHTMLAsString))
|
||||
}
|
|
@ -25,37 +25,36 @@
|
|||
package pastes
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
||||
// local
|
||||
"github.com/pztrn/fastpastebin/context"
|
||||
)
|
||||
|
||||
var regexInts = regexp.MustCompile("[0-9]+")
|
||||
var (
|
||||
c *context.Context
|
||||
)
|
||||
|
||||
var ctx *context.Context
|
||||
|
||||
// New initializes pastes package and adds necessary HTTP and API
|
||||
// New initializes pastes package and adds neccessary HTTP and API
|
||||
// endpoints.
|
||||
func New(cc *context.Context) {
|
||||
ctx = cc
|
||||
c = cc
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// HTTP endpoints.
|
||||
////////////////////////////////////////////////////////////
|
||||
// New paste.
|
||||
ctx.Echo.POST("/paste/", pastePOSTWebInterface)
|
||||
c.Echo.POST("/paste/", pastePOST)
|
||||
|
||||
// Show public paste.
|
||||
ctx.Echo.GET("/paste/:id", pasteGETWebInterface)
|
||||
c.Echo.GET("/paste/:id", pasteGET)
|
||||
// Show RAW representation of public paste.
|
||||
ctx.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface)
|
||||
c.Echo.GET("/paste/:id/raw", pasteRawGET)
|
||||
|
||||
// Show private paste.
|
||||
ctx.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface)
|
||||
c.Echo.GET("/paste/:id/:timestamp", pasteGET)
|
||||
// Show RAW representation of private paste.
|
||||
ctx.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGETWebInterface)
|
||||
c.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGET)
|
||||
// Verify access to passworded paste.
|
||||
ctx.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
|
||||
ctx.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
|
||||
c.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
|
||||
c.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
|
||||
|
||||
// Pastes list.
|
||||
ctx.Echo.GET("/pastes/", pastesGET)
|
||||
ctx.Echo.GET("/pastes/:page", pastesGET)
|
||||
c.Echo.GET("/pastes/", pastesGET)
|
||||
c.Echo.GET("/pastes/:page", pastesGET)
|
||||
}
|
|
@ -22,64 +22,56 @@
|
|||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package structs
|
||||
package pastes
|
||||
|
||||
import (
|
||||
// stdlib
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
// other
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
// PasteKeepForever indicates that paste should be kept forever.
|
||||
PasteKeepForever = 0
|
||||
// PasteKeepForMinutes indicates that saved timeout is in minutes.
|
||||
PasteKeepForMinutes = 1
|
||||
// PasteKeepForHours indicates that saved timeout is in hours.
|
||||
PasteKeepForHours = 2
|
||||
// PasteKeepForDays indicates that saved timeout is in days.
|
||||
PasteKeepForDays = 3
|
||||
// PasteKeepForMonths indicates that saved timeout is in months.
|
||||
PasteKeepForMonths = 4
|
||||
PASTE_KEEP_FOR_MINUTES = 1
|
||||
PASTE_KEEP_FOR_HOURS = 2
|
||||
PASTE_KEEP_FOR_DAYS = 3
|
||||
PASTE_KEEP_FOR_MONTHS = 4
|
||||
|
||||
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
)
|
||||
|
||||
// PasteKeepsCorrelation is a correlation map between database representation
|
||||
// and passed data representation.
|
||||
var PasteKeepsCorrelation = map[string]int{
|
||||
"M": PasteKeepForMinutes,
|
||||
"h": PasteKeepForHours,
|
||||
"d": PasteKeepForDays,
|
||||
"m": PasteKeepForMonths,
|
||||
"forever": PasteKeepForever,
|
||||
}
|
||||
var (
|
||||
PASTE_KEEPS_CORELLATION = map[string]int{
|
||||
"M": PASTE_KEEP_FOR_MINUTES,
|
||||
"h": PASTE_KEEP_FOR_HOURS,
|
||||
"d": PASTE_KEEP_FOR_DAYS,
|
||||
"m": PASTE_KEEP_FOR_MONTHS,
|
||||
}
|
||||
)
|
||||
|
||||
// Paste represents paste itself.
|
||||
type Paste struct {
|
||||
CreatedAt *time.Time `db:"created_at" json:"created_at"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Data string `db:"data" json:"data"`
|
||||
Language string `db:"language" json:"language"`
|
||||
Password string `db:"password" json:"password"`
|
||||
PasswordSalt string `db:"password_salt" json:"password_salt"`
|
||||
ID int `db:"id" json:"id"`
|
||||
KeepFor int `db:"keep_for" json:"keep_for"`
|
||||
KeepForUnitType int `db:"keep_for_unit_type" json:"keep_for_unit_type"`
|
||||
Private bool `db:"private" json:"private"`
|
||||
ID int `db:"id"`
|
||||
Title string `db:"title"`
|
||||
Data string `db:"data"`
|
||||
CreatedAt *time.Time `db:"created_at"`
|
||||
KeepFor int `db:"keep_for"`
|
||||
KeepForUnitType int `db:"keep_for_unit_type"`
|
||||
Language string `db:"language"`
|
||||
Private bool `db:"private"`
|
||||
Password string `db:"password"`
|
||||
PasswordSalt string `db:"password_salt"`
|
||||
}
|
||||
|
||||
// CreatePassword creates password for current paste.
|
||||
func (p *Paste) CreatePassword(password string) error {
|
||||
// Create salt - random string.
|
||||
// Yes, it is insecure. Should be refactored!
|
||||
//nolint:gosec
|
||||
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
saltBytes := make([]byte, 64)
|
||||
|
||||
for i := range saltBytes {
|
||||
saltBytes[i] = charset[seededRand.Intn(len(charset))]
|
||||
}
|
||||
|
@ -90,10 +82,8 @@ func (p *Paste) CreatePassword(password string) error {
|
|||
// Create crypted password and hash it.
|
||||
passwordCrypted, err := scrypt.Key([]byte(password), []byte(p.PasswordSalt), 131072, 8, 1, 64)
|
||||
if err != nil {
|
||||
//nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
passwordHashBytes := sha256.Sum256(passwordCrypted)
|
||||
p.Password = fmt.Sprintf("%x", passwordHashBytes)
|
||||
|
||||
|
@ -103,23 +93,19 @@ func (p *Paste) CreatePassword(password string) error {
|
|||
// GenerateCryptedCookieValue generates crypted cookie value for paste.
|
||||
func (p *Paste) GenerateCryptedCookieValue() string {
|
||||
cookieValueCrypted, _ := scrypt.Key([]byte(p.Password), []byte(p.PasswordSalt), 131072, 8, 1, 64)
|
||||
|
||||
return fmt.Sprintf("%x", sha256.Sum256(cookieValueCrypted))
|
||||
}
|
||||
|
||||
func (p *Paste) GetExpirationTime() time.Time {
|
||||
var expirationTime time.Time
|
||||
|
||||
switch p.KeepForUnitType {
|
||||
case PasteKeepForever:
|
||||
expirationTime = time.Now().UTC().Add(time.Hour * 1)
|
||||
case PasteKeepForMinutes:
|
||||
case PASTE_KEEP_FOR_MINUTES:
|
||||
expirationTime = p.CreatedAt.Add(time.Minute * time.Duration(p.KeepFor))
|
||||
case PasteKeepForHours:
|
||||
case PASTE_KEEP_FOR_HOURS:
|
||||
expirationTime = p.CreatedAt.Add(time.Hour * time.Duration(p.KeepFor))
|
||||
case PasteKeepForDays:
|
||||
case PASTE_KEEP_FOR_DAYS:
|
||||
expirationTime = p.CreatedAt.Add(time.Hour * 24 * time.Duration(p.KeepFor))
|
||||
case PasteKeepForMonths:
|
||||
case PASTE_KEEP_FOR_MONTHS:
|
||||
expirationTime = p.CreatedAt.Add(time.Hour * 24 * 30 * time.Duration(p.KeepFor))
|
||||
}
|
||||
|
||||
|
@ -131,7 +117,11 @@ func (p *Paste) IsExpired() bool {
|
|||
curTime := time.Now().UTC()
|
||||
expirationTime := p.GetExpirationTime()
|
||||
|
||||
return curTime.Sub(expirationTime).Seconds() > 0
|
||||
if curTime.Sub(expirationTime).Seconds() > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// VerifyPassword verifies that provided password is valid.
|
||||
|
@ -141,9 +131,12 @@ func (p *Paste) VerifyPassword(password string) bool {
|
|||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
passwordHashBytes := sha256.Sum256(passwordCrypted)
|
||||
providedPassword := fmt.Sprintf("%x", passwordHashBytes)
|
||||
|
||||
return providedPassword == p.Password
|
||||
if providedPassword == p.Password {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue