Compare commits

...

78 Commits

Author SHA1 Message Date
Stanislav Nikitin b87921c811
README update.
continuous-integration/drone/push Build is passing Details
2022-08-14 16:51:17 +05:00
Stanislav Nikitin 74ea9b6863
Prepare to release 0.4.1.
continuous-integration/drone/push Build is passing Details
2022-08-14 16:26:11 +05:00
Stanislav Nikitin 30f8b4975c
Get rid of flagger.
continuous-integration/drone/push Build is passing Details
2022-08-14 16:20:16 +05:00
Stanislav Nikitin dcd361de1f
Update dependencies, fix footer link.
continuous-integration/drone/push Build is passing Details
2022-08-14 16:02:16 +05:00
Stanislav Nikitin 2ec0e28243
Make linter happy and move to os from io/ioutil for reading/writing.
continuous-integration/drone/push Build is passing Details
2022-08-14 15:49:05 +05:00
Stanislav Nikitin df5671586e
Update images used for building containers and code test/lint, fill CHANGELOG.
continuous-integration/drone/push Build is failing Details
2022-08-14 15:34:47 +05:00
Stanislav Nikitin 59dafc373f
Update chroma to 2.2.0, line number links appeared.
continuous-integration/drone/push Build is failing Details
2022-08-14 14:57:54 +05:00
Stanislav Nikitin 25489dc103
Forgotten files for previous commit.
continuous-integration/drone/push Build is failing Details
2022-08-14 14:49:31 +05:00
Stanislav Nikitin 1dc6dfe00e
Update README and mention where bugreporting should take place.
continuous-integration/drone/push Build is failing Details
Also added markdownlint configuration.
2022-08-14 14:47:28 +05:00
Stanislav Nikitin 02e933efed
Use mirrorred images in Dockerfile.
continuous-integration/drone/push Build is passing Details
2022-06-28 22:19:42 +05:00
Stanislav Nikitin d2b3304a5a
As it isn't Drone official plugins now, containers with dind should be privileged.
continuous-integration/drone/push Build is failing Details
2022-06-28 21:44:23 +05:00
Stanislav Nikitin 9164c53c54
Force docker plugin version.
continuous-integration/drone/push Build is failing Details
2022-06-28 21:38:09 +05:00
Stanislav Nikitin 18734ebd18
Switch to mirrorred images for Drone CI.
continuous-integration/drone/push Build encountered an error Details
2022-06-28 21:19:35 +05:00
Stanislav Nikitin 6b04a1dcd5
Update Dockerfile to use latest versions of Golang and Alpine.
continuous-integration/drone/push Build is passing Details
2022-06-26 22:52:50 +05:00
Stanislav Nikitin 1cb93c7d97
Update Drone configuration.
continuous-integration/drone/push Build is passing Details
2022-06-26 22:49:37 +05:00
Stanislav Nikitin f8f0302564
Make linter happy and update dependencies.
continuous-integration/drone/push Build is failing Details
2022-06-26 22:26:39 +05:00
Stanislav Nikitin 7b6a425908
Update Drone configuration. 2022-06-26 22:10:53 +05:00
Stanislav Nikitin 591c24bab7
Update Drone configuration. 2022-06-26 22:08:23 +05:00
Stanislav Nikitin d85c9cb53c
Update Drone configuration. 2022-06-26 22:04:03 +05:00
Stanislav Nikitin 2b44a60ee7
The Great Linting Fixes, Drone configuration fix (again) and flatfile changes.
Great linting fixes has been applied, thanks to golangci-lint for
extensive reporting.

Fixed Drone configuration to use array for when-branch statement in
Docker plugin.

Flatfile storage from now will write files with 0600 permission for
greater security.
2021-11-20 22:19:58 +05:00
Stanislav Nikitin 218e0bf667
Remove -mod=vendor from golangci-lint GOFLAGS. 2021-11-20 22:00:52 +05:00
Stanislav Nikitin 006eb6e72a
Use newer golang with Drone and update badge in README. 2021-11-20 22:00:02 +05:00
Stanislav Nikitin 5eae5595e9
Drone fixes. 2021-11-20 02:23:47 +05:00
Stanislav Nikitin 0f47434f60
Move from fileb0x to embed.FS. Fixes #22.
Also removed unused fontawesome JS.
2021-06-16 06:22:33 +05:00
Stanislav Nikitin 383233202e
Fix registry logging in by CI when uploading Docker image. 2021-06-15 00:16:44 +05:00
Stanislav Nikitin 968d945205
More Greater Linting. 2021-06-15 00:11:58 +05:00
Stanislav Nikitin 6ea6e2e144
The Great Sources Linting. 2021-06-14 23:48:34 +05:00
Stanislav Nikitin 3265c5a4b2
Proper image-alias for DIND service in CI. 2021-06-14 23:10:35 +05:00
Stanislav Nikitin 1210ecb510
Fix CI config. 2021-06-14 23:08:54 +05:00
Stanislav Nikitin dec022e460
Update CI configuration. 2021-06-14 23:05:59 +05:00
Stanislav Nikitin 79791ef228
Update golang versions everywhere, use alpine 3.13 for image, remove vendor dir. 2021-06-14 22:59:21 +05:00
Stanislav Nikitin a0f896dc53
CI config update. 2021-01-09 06:23:28 +05:00
Stanislav Nikitin fa7a79310d
Assets update. 2021-01-09 06:15:38 +05:00
Stanislav Nikitin ce962ddd00
Version bump. 2021-01-09 06:14:16 +05:00
Stanislav Nikitin 164a37d41f
A pretty basic pastes cleanup procedure.
Fixes #16.
2021-01-09 06:09:26 +05:00
Stanislav Nikitin 3ebff29113
Linting fixes about struct alignment. 2021-01-08 12:01:53 +05:00
Stanislav Nikitin 7f04a9a7bb
Typo fix and missed dependencies files. 2021-01-08 12:00:38 +05:00
Stanislav Nikitin a2a21482e2
Dependencies update and fix for line linking.
Fixes #20.
2021-01-08 11:56:35 +05:00
Stanislav Nikitin ffb42f43eb
README update. 2020-02-29 23:51:23 +05:00
Stanislav Nikitin 3f9801b1bd
Update chroma and make line numbers linkable.
Yet no links will be generated.

Related to #20.
2020-02-29 23:39:09 +05:00
Stanislav Nikitin 825fd724ff
Some logging refactoring.
Fixes #14.
2020-02-29 23:30:44 +05:00
Stanislav Nikitin 5f58741159
Added maximum request body size limiting.
Fixes #19.
2020-02-29 22:49:44 +05:00
Stanislav Nikitin 47672c586d
Linter configuration fixes and linting issues fixes.
Fixes #17.
2020-02-29 22:41:06 +05:00
Stanislav Nikitin 11897d0e1a
Drone CI recipe fix. 2019-12-22 01:24:13 +05:00
Stanislav Nikitin 02cea49fc4 Merge branch 'linting' of fastpastebin/fastpastebin into master 2019-12-21 20:19:21 +00:00
Stanislav Nikitin a52b18ffe4
Linting. 2019-12-22 01:17:18 +05:00
Stanislav Nikitin da4bc379d8
Typo in commend and switching to discordrone for notifications. 2019-12-22 00:56:43 +05:00
Stanislav Nikitin 9c9f0c1f68 Merge branch 'linting' of fastpastebin/fastpastebin into master 2019-10-13 11:28:15 +00:00
Stanislav Nikitin 6a787e7e23
Linting things. 2019-10-13 16:26:52 +05:00
Stanislav Nikitin cdc8ecf49b Merge branch 'readme-update' of fastpastebin/fastpastebin into master 2019-10-13 09:52:06 +00:00
Stanislav Nikitin bd981023da
README update. 2019-10-13 14:50:11 +05:00
Stanislav Nikitin a982e07faf Fixed improper tooltip (popups) rendering. Fixes #8. (#11) 2019-10-13 09:25:29 +00:00
Stanislav Nikitin 0bbb415852
Drone pipelines update. 2019-10-13 14:02:33 +05:00
Vladimir Hodakov 6207229e9b Move to Go modules (#10) 2019-10-13 08:55:38 +00:00
Stanislav Nikitin 2159aadfcb
Fixed Safari stuck when clicking on dropdowns by updating Bulma.
Fixes #4.
2019-10-12 15:10:13 +05:00
Stanislav Nikitin 3ebf9b653e
Put docker image into approriate repo. Related to #6. 2019-10-12 14:30:20 +05:00
Stanislav Nikitin 4d60a9b1b6
Switching to Drone CI and giredore. 2019-10-12 14:26:46 +05:00
Stanislav Nikitin e042c4414f
Moved from gitlab.com to Gitea with giredore. 2019-10-12 14:18:35 +05:00
Stanislav Nikitin 333fc1d12a
CI configuration. 2019-09-25 18:47:51 +05:00
Stanislav Nikitin 7f2174a33e
Moved Dockerfile and docker-compose.yml to top directory. 2019-09-25 18:45:50 +05:00
Stanislav Nikitin e26a466efc Merge branch 'feature/docker' into 'master'
Dockerize the app

See merge request pztrn/fastpastebin!1
2019-07-23 04:30:22 +00:00
Timur Demin d9a46aa5b3
Make changes proposed by @pztrn 2019-07-20 14:16:06 +05:00
Timur Demin dbf82e213b
Dockerize the app 2019-07-08 08:41:03 +05:00
Stanislav Nikitin 4f01e2f5ce
Logger refactoring, part 1. 2019-04-13 00:05:22 +05:00
Stanislav Nikitin 7281b9be65
Refactoring for pastes domain for code reusing and better structuring. 2019-04-12 23:29:42 +05:00
Stanislav Nikitin 19b5ef3d9f
Fastfix for previous two commits (sorry) and chroma update.
Well, doing "git add ." not from repository's root is a bad idea.

Updated chroma dependency, added more languages to syntax highlighting.
2019-03-07 08:43:28 +05:00
Stanislav Nikitin 3fe51fc6c5
Dependencies update and database not available crash fix.
Added forgotten dependencies update.

Fixed fastpastebin crash when database isn't available and added cute
error screen for informing users about such condition (both HTML and raw).
2019-03-07 08:39:12 +05:00
Stanislav Nikitin 19a3a5004c
The Huge Refactoring. 2019-03-07 07:56:50 +05:00
Stanislav Nikitin 0bf20cd2c9
Assets update. 2018-12-01 04:46:27 +05:00
Stanislav Nikitin 35c217fe46
Builder script and gitignore update. 2018-12-01 04:23:15 +05:00
Stanislav Nikitin 849c72b238
Version bump. 2018-12-01 03:55:11 +05:00
Stanislav Nikitin c565ec8f21 Fixed timezone things with postgresql and made "Forever" work. 2018-12-01 03:49:04 +05:00
Stanislav Nikitin aa5e11329f PostgreSQL support. 2018-12-01 03:23:52 +05:00
Stanislav Nikitin b6556d6e90 Moved database migrations for MySQL dialect into approriate package. 2018-12-01 02:29:14 +05:00
Stanislav Nikitin 400ce0db0c Quick and dirty fix to get database connection reestablish if connection was lost. 2018-12-01 02:23:36 +05:00
Stanislav Nikitin fc8f4e9d8b Switch to Gitlab. 2018-12-01 02:16:06 +05:00
Stanislav Nikitin 2beb9ecef9 Version bump - we're 0.2.0! 2018-05-27 12:27:43 +05:00
Stanislav Nikitin 0cca0f453f Database dialects, proper database shutdown, pagination in configuration.
Added possibility to use different database (storage) backends. Currently
supported: flatfiles and mysql. Fixes #2.

Added database shutdown call. This call will properly shutdown database
connections (in case of RDBMS) or flush pastes index cache on disk (in
case of flatfiles).

De-hardcoded pagination count. Fixes #12.
2018-05-27 12:25:01 +05:00
818 changed files with 3246 additions and 243723 deletions

58
.drone.yml Normal file
View File

@ -0,0 +1,58 @@
---
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

8
.gitignore vendored
View File

@ -1 +1,7 @@
examples/fastpastebin.yaml
examples/fastpastebin.yaml
dist/
data/
vendor/
.idea
.vscode
*DS_Store*

38
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,38 @@
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

35
.golangci.yml Normal file
View File

@ -0,0 +1,35 @@
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"

5
.markdownlint.json Normal file
View File

@ -0,0 +1,5 @@
{
"line-length": false,
"first-line-h1": false,
"no-duplicate-header": false
}

View File

@ -1,12 +1,84 @@
# Changelog
``[A]`` - added
``[F]`` - fixed
``[R]`` - removed
All notable changes to this project will be documented in this file.
---
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).
## 0.1.0
## [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
First normal release. Fast Paste Bin is able to handle public, private
and passworded pastes.
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

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
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" ]

187
Gopkg.lock generated
View File

@ -1,187 +0,0 @@
# 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

View File

@ -1,34 +0,0 @@
# 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
View File

@ -1,78 +1,90 @@
[Chat on Keybase.io](https://keybase.io/team/fastpastebin)
# Fast Pastebin
Easy-to-use-and-install pastebin software written in Go. No bells or
whistles, no websockets and even NO JAVASCRIPT!
[![Build Status](https://ci.code.pztrn.name/api/badges/apps/fastpastebin/status.svg)](https://ci.code.pztrn.name/apps/fastpastebin)
# Current functionality.
Easy-to-use-and-install pastebin software written in Go. No bells or whistles, no websockets and even NO JAVASCRIPT!
**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
* Create and view public and private pastes.
* Syntax highlighting.
* Pastes expiration.
* Passwords for pastes.
* Multiple storage backends. Currently: ``flatfiles``, ``mysql`` and ``postgresql``.
# Caveats.
## Caveats
* No links at lines numbers. See https://github.com/alecthomas/chroma/issues/132
* Not known at this moment.
# Installation and updating
## Installation and updating
Just issue:
```bash
CGO_ENABLED=0 go install go.dev.pztrn.name/fastpastebin/cmd/fastpastebin@VERSION
```
go get -u -v github.com/pztrn/fastpastebin/cmd/fastpastebin
```
Replace `VERSION` with a [tag of your choice](https://code.pztrn.name/apps/fastpastebin/releases).
This command can be used to update Fast Paste Bin.
**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.
Also Fast Paste Bin is dockerized, see [here](https://code.pztrn.name/apps/-/packages/container/fastpastebin) for instructions.
# Configuration.
Compose file with resources limits, as used by me:
Take a look at [example configuration file](examples/fastpastebin.yaml.dist)
which contains all supported options and their descriptions.
```yaml
---
version: "2.4"
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
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
```
Also if you're changed list of assets (by creating or deleting them) be sure
to fix files list in ``fileb0x.yml`` file!
## Configuration
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:
Take a look at [example configuration file](examples/fastpastebin.yaml.dist) which contains all supported options and their descriptions.
* Imports should be organized in 3 groups: stdlib, local, other. See
https://github.com/pztrn/fastpastebin/blob/master/pastes/api_http.go for
example.
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.
* We're not forcing any limits on line length for code, only for comments,
they should be 72-76 chars long.
## Developing
# ToDo
### Branching, versions, etc
This is a ToDo list which isn't sorted by any parameter at all. Just a list
of tasks you can help with.
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.
* User CP.
* Files uploading.
* Passwords for files.
* Pastes forking and revisioning (like git or github gists).
* Possibility to copy-paste-edit WISYWIG content.
* CLI client for pastes and files uploading.
* Possibility to copy-paste-edit WYSIWYG content.
* CLI client for pastes and files uploading.

View File

@ -1,51 +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 http
import (
// local
"github.com/pztrn/fastpastebin/api/http/static"
"github.com/pztrn/fastpastebin/context"
// other
"github.com/labstack/echo"
)
var (
c *context.Context
)
// New initializes basic HTTP API, which shows only index page and serves
// static files.
func New(cc *context.Context) {
c = cc
c.Logger.Info().Msg("Initializing HTTP API...")
// Static files.
c.Echo.GET("/static/*", echo.WrapHandler(static.Handler))
// Index.
c.Echo.GET("/", indexGet)
}

View File

@ -1,182 +0,0 @@
// Code generated by fileb0x at "2018-05-26 13:58:30.612342807 +0500 +05 m=+0.013490397" from config file "fileb0x.yml" DO NOT EDIT.
// modification hash(e9e210106a4d77680942064c40d06a3e.522a9cc525fa984df98098d4c3992752)
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
}

View File

@ -1,35 +0,0 @@
// Code generaTed by fileb0x at "2018-05-26 13:29:06.05698436 +0500 +05 m=+0.013970976" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2018-05-26 13:29:04.002355958 +0500 +05)
// original path: assets/error.html
package static
import (
"os"
)
// FileErrorHTML is "/error.html"
var FileErrorHTML = []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\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x62\x6f\x78\x20\x68\x61\x73\x2d\x62\x61\x63\x6b\x67\x72\x6f\x75\x6e\x64\x2d\x77\x61\x72\x6e\x69\x6e\x67\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\x64\x69\x76\x3e\x0a\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\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)
}
}

View File

@ -1,35 +0,0 @@
// Code generaTed by fileb0x at "2018-05-26 12:55:42.087956217 +0500 +05 m=+0.009637095" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2018-05-26 12:54:22.9990735 +0500 +05)
// original path: assets/footer.html
package static
import (
"os"
)
// FileFooterHTML is "/footer.html"
var FileFooterHTML = []byte("\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6f\x6e\x74\x61\x69\x6e\x65\x72\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6f\x6e\x74\x65\x6e\x74\x20\x68\x61\x73\x2d\x74\x65\x78\x74\x2d\x63\x65\x6e\x74\x65\x72\x65\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x74\x72\x6f\x6e\x67\x3e\x46\x61\x73\x74\x20\x70\x61\x73\x74\x65\x20\x62\x69\x6e\x3c\x2f\x73\x74\x72\x6f\x6e\x67\x3e\x20\x76\x65\x72\x73\x69\x6f\x6e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x74\x72\x6f\x6e\x67\x3e\x7b\x76\x65\x72\x73\x69\x6f\x6e\x7d\x3c\x2f\x73\x74\x72\x6f\x6e\x67\x3e\x20\x62\x79\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x70\x7a\x74\x72\x6e\x2e\x6e\x61\x6d\x65\x22\x3e\x53\x74\x61\x6e\x69\x73\x6c\x61\x76\x20\x4e\x2e\x20\x61\x6b\x61\x20\x70\x7a\x74\x72\x6e\x3c\x2f\x61\x3e\x2e\x20\x54\x68\x65\x20\x73\x6f\x75\x72\x63\x65\x20\x63\x6f\x64\x65\x20\x69\x73\x20\x6c\x69\x63\x65\x6e\x73\x65\x64\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6f\x70\x65\x6e\x73\x6f\x75\x72\x63\x65\x2e\x6f\x72\x67\x2f\x6c\x69\x63\x65\x6e\x73\x65\x73\x2f\x6d\x69\x74\x2d\x6c\x69\x63\x65\x6e\x73\x65\x2e\x70\x68\x70\x22\x3e\x4d\x49\x54\x3c\x2f\x61\x3e\x2e\x20\x47\x65\x74\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x67\x69\x74\x68\x75\x62\x2e\x63\x6f\x6d\x2f\x70\x7a\x74\x72\x6e\x2f\x66\x61\x73\x74\x70\x61\x73\x74\x65\x62\x69\x6e\x22\x3e\x73\x6f\x75\x72\x63\x65\x20\x6f\x72\x20\x62\x69\x6e\x61\x72\x79\x20\x72\x65\x6c\x65\x61\x73\x65\x73\x20\x68\x65\x72\x65\x3c\x2f\x61\x3e\x21\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x70\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, "/footer.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
if err != nil {
panic(err)
}
_, err = f.Write(FileFooterHTML)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,35 +0,0 @@
// Code generaTed by fileb0x at "2018-05-26 12:42:22.452857215 +0500 +05 m=+0.054711585" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2018-05-26 12:41:36.015758918 +0500 +05)
// original path: assets/main.html
package static
import (
"os"
)
// FileMainHTML is "/main.html"
var FileMainHTML = []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\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\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\x74\x6f\x6f\x6c\x74\x69\x70\x2d\x31\x2e\x30\x2e\x34\x2e\x6d\x69\x6e\x2e\x63\x73\x73\x22\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\x7b\x6e\x61\x76\x69\x67\x61\x74\x69\x6f\x6e\x7d\x20\x7b\x64\x6f\x63\x75\x6d\x65\x6e\x74\x42\x6f\x64\x79\x7d\x0a\x3c\x2f\x62\x6f\x64\x79\x3e\x0a\x3c\x66\x6f\x6f\x74\x65\x72\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x6f\x6f\x74\x65\x72\x22\x3e\x0a\x20\x20\x20\x20\x7b\x66\x6f\x6f\x74\x65\x72\x7d\x0a\x3c\x2f\x66\x6f\x6f\x74\x65\x72\x3e\x0a\x0a\x3c\x2f\x68\x74\x6d\x6c\x3e")
func init() {
f, err := FS.OpenFile(CTX, "/main.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
if err != nil {
panic(err)
}
_, err = f.Write(FileMainHTML)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
}

View File

@ -1,35 +0,0 @@
// Code generaTed by fileb0x at "2018-05-26 13:58:30.613412852 +0500 +05 m=+0.014560424" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2018-05-26 13:57:14.88231462 +0500 +05)
// original path: assets/navigation.html
package static
import (
"os"
)
// FileNavigationHTML is "/navigation.html"
var FileNavigationHTML = []byte("\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\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\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\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\x50\x61\x73\x74\x65\x73\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\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\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\x3c\x2f\x64\x69\x76\x3e\x0a\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\x65\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x6e\x61\x76\x3e")
func init() {
f, err := FS.OpenFile(CTX, "/navigation.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
if err != nil {
panic(err)
}
_, err = f.Write(FileNavigationHTML)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
}

View File

@ -1,35 +0,0 @@
// 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)
}
}

View File

@ -1,35 +0,0 @@
// 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)
}
}

View File

@ -1,35 +0,0 @@
// 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)
}
}

View File

@ -1,35 +0,0 @@
// 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

View File

@ -1,35 +0,0 @@
// Code generaTed by fileb0x at "2018-05-26 13:24:21.508025661 +0500 +05 m=+0.014883772" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2018-05-26 13:24:18.605696269 +0500 +05)
// original path: assets/paste.html
package static
import (
"os"
)
// FilePasteHTML is "/paste.html"
var FilePasteHTML = []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\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\x3c\x74\x61\x62\x6c\x65\x20\x63\x6c\x61\x73\x73\x3d\x22\x74\x61\x62\x6c\x65\x20\x69\x73\x2d\x62\x6f\x72\x64\x65\x72\x65\x64\x20\x69\x73\x2d\x73\x74\x72\x69\x70\x65\x64\x20\x69\x73\x2d\x6e\x61\x72\x72\x6f\x77\x20\x69\x73\x2d\x68\x6f\x76\x65\x72\x61\x62\x6c\x65\x20\x69\x73\x2d\x66\x75\x6c\x6c\x77\x69\x64\x74\x68\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x23\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x54\x69\x74\x6c\x65\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x4c\x61\x6e\x67\x75\x61\x67\x65\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x50\x61\x73\x74\x65\x64\x20\x6f\x6e\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x57\x69\x6c\x6c\x20\x65\x78\x70\x69\x72\x65\x20\x6f\x6e\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x50\x61\x73\x74\x65\x20\x74\x79\x70\x65\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x62\x6f\x64\x79\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x49\x44\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x54\x69\x74\x6c\x65\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x4c\x61\x6e\x67\x75\x61\x67\x65\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x44\x61\x74\x65\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x45\x78\x70\x69\x72\x61\x74\x69\x6f\x6e\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x54\x79\x70\x65\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x20\x63\x6f\x6c\x73\x70\x61\x6e\x3d\x22\x36\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x62\x75\x74\x74\x6f\x6e\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x70\x61\x73\x74\x65\x2f\x7b\x70\x61\x73\x74\x65\x49\x44\x7d\x2f\x7b\x70\x61\x73\x74\x65\x54\x73\x7d\x72\x61\x77\x22\x3e\x56\x69\x65\x77\x20\x72\x61\x77\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x62\x6f\x64\x79\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x61\x62\x6c\x65\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e\x0a\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x70\x61\x73\x74\x65\x2d\x64\x61\x74\x61\x22\x3e\x0a\x20\x20\x20\x20\x7b\x70\x61\x73\x74\x65\x64\x61\x74\x61\x7d\x0a\x3c\x2f\x64\x69\x76\x3e")
func init() {
f, err := FS.OpenFile(CTX, "/paste.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
if err != nil {
panic(err)
}
_, err = f.Write(FilePasteHTML)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
}

View File

@ -1,35 +0,0 @@
// Code generaTed by fileb0x at "2018-05-26 13:47:21.417964779 +0500 +05 m=+0.019030606" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2018-05-26 13:41:01.75233841 +0500 +05)
// original path: assets/pastelist_list.html
package static
import (
"os"
)
// FilePastelistListHTML is "/pastelist_list.html"
var FilePastelistListHTML = []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\x64\x69\x76\x3e\x0a\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\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x73\x74\x65\x73\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\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\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\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)
}
}

View File

@ -1,35 +0,0 @@
// 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

View File

@ -1,35 +0,0 @@
// 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

8
assets/assets.go Normal file
View File

@ -0,0 +1,8 @@
//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

View File

@ -1,9 +0,0 @@
#paste-contents {
font-family: monospace;
font-size: 0.9rem;
height: 90vh;
}
.paste-data {
font-size: 0.9rem;
}

View File

@ -0,0 +1,6 @@
<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>

View File

@ -5,7 +5,7 @@
<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://github.com/pztrn/fastpastebin">source or binary releases here</a>!
<a href="https://code.pztrn.name/apps/fastpastebin">source or binary releases here</a>!
</p>
</div>
</div>
</div>

View File

@ -38,14 +38,13 @@
</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!">
<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" data-tooltip="If you'll enter password here - 'Private
paste with unique URL' checkbox will be assumed as checked.">
<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">
@ -79,4 +78,4 @@
</div>
</div>
</form>
</section>
</section>

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<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">
<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>
@ -17,4 +17,4 @@
{footer}
</footer>
</html>
</html>

1
assets/static/css/bulma-0.9.4.min.css vendored Normal file

File diff suppressed because one or more lines are too long

2
assets/static/css/bulma-tooltip-1.2.min.css vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
#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 Executable file
View File

@ -0,0 +1,26 @@
#!/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

View File

@ -25,53 +25,54 @@
package main
import (
// stdlib
"flag"
"os"
"os/signal"
"syscall"
// 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"
"github.com/pztrn/fastpastebin/templater"
"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"
)
func main() {
c := context.New()
c.Initialize()
appCtx := context.New()
appCtx.Initialize()
c.Logger.Info().Msg("Starting Fast Pastebin...")
appCtx.Logger.Info().Msg("Starting Fast Pastebin...")
// Here goes initial initialization for packages that want CLI flags
// to be added.
// Parse flags.
c.Flagger.Parse()
flag.Parse()
// Continue loading.
c.LoadConfiguration()
database.New(c)
c.Database.Initialize()
migrations.New(c)
migrations.Migrate()
templater.Initialize(c)
api.New(c)
api.InitializeAPI()
appCtx.LoadConfiguration()
appCtx.InitializePost()
database.New(appCtx)
appCtx.Database.Initialize()
templater.Initialize(appCtx)
captcha.New(c)
pastes.New(c)
captcha.New(appCtx)
dbnotavailable.New(appCtx)
indexpage.New(appCtx)
pastes.New(appCtx)
// CTRL+C handler.
signalHandler := make(chan os.Signal, 1)
shutdownDone := make(chan bool, 1)
signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM)
go func() {
<-signalHandler
c.Shutdown()
appCtx.Shutdown()
shutdownDone <- true
}()

View File

@ -1,72 +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 (
// 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
}

37
docker-compose.yml Normal file
View File

@ -0,0 +1,37 @@
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

View File

@ -0,0 +1,22 @@
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

10
docker/nginx.conf Normal file
View File

@ -0,0 +1,10 @@
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;
}
}

7
docker/set_docker_tag.sh Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
if [[ $CI_BUILD_REF_NAME == "master" ]]; then
export DOCKER_TAG=latest;
else
export DOCKER_TAG="${CI_BUILD_REF_NAME}";
fi

View File

@ -22,32 +22,24 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package api
package dbnotavailable
import (
// other
"net/http"
"github.com/labstack/echo"
"go.dev.pztrn.name/fastpastebin/internal/templater"
)
// 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")
// Database not available error page.
func dbNotAvailableGet(ec echo.Context) error {
htmlData := templater.GetTemplate(ec, "database_not_available.html", nil)
next(ec)
return nil
//nolint:wrapcheck
return ec.HTML(http.StatusInternalServerError, htmlData)
}
// 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)
}
}
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.")
}

View File

@ -0,0 +1,40 @@
// 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 dbnotavailable
import (
"go.dev.pztrn.name/fastpastebin/internal/context"
)
var ctx *context.Context
// 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)
}

View File

@ -22,19 +22,18 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package json
package indexpage
import (
// local
"github.com/pztrn/fastpastebin/context"
"go.dev.pztrn.name/fastpastebin/internal/context"
)
var (
c *context.Context
)
var ctx *context.Context
// New initializes basic JSON API.
// New initializes pastes package and adds necessary HTTP and API
// endpoints.
func New(cc *context.Context) {
c = cc
c.Logger.Info().Msg("Initializing JSON API...")
ctx = cc
ctx.Echo.GET("/", indexGet)
}

View File

@ -22,27 +22,31 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package http
package indexpage
import (
// stdlib
"net/http"
// local
"github.com/pztrn/fastpastebin/captcha"
"github.com/pztrn/fastpastebin/templater"
// other
"github.com/alecthomas/chroma/lexers"
"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/templater"
)
// Index of this site.
func indexGet(ec echo.Context) error {
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")
}
// Generate list of available languages to highlight.
availableLexers := lexers.Names(false)
var availableLexersSelectOpts = "<option value='text'>Text</option><option value='autodetect'>Auto detect</option><option disabled>-----</option>"
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>"
}
@ -50,7 +54,8 @@ func indexGet(ec echo.Context) error {
// Captcha.
captchaString := captcha.NewCaptcha()
htmlData := templater.GetTemplate(ec, "index.html", map[string]string{"lexers": availableLexersSelectOpts, "captchaString": captchaString})
htmlData := templater.GetTemplate(ectx, "index.html", map[string]string{"lexers": availableLexersSelectOpts, "captchaString": captchaString})
return ec.HTML(http.StatusOK, htmlData)
//nolint:wrapcheck
return ectx.HTML(http.StatusOK, htmlData)
}

View File

@ -25,36 +25,37 @@
package pastes
import (
// local
"github.com/pztrn/fastpastebin/context"
"regexp"
"go.dev.pztrn.name/fastpastebin/internal/context"
)
var (
c *context.Context
)
var regexInts = regexp.MustCompile("[0-9]+")
// New initializes pastes package and adds neccessary HTTP and API
var ctx *context.Context
// New initializes pastes package and adds necessary HTTP and API
// endpoints.
func New(cc *context.Context) {
c = cc
ctx = cc
////////////////////////////////////////////////////////////
// HTTP endpoints.
////////////////////////////////////////////////////////////
// New paste.
c.Echo.POST("/paste/", pastePOST)
ctx.Echo.POST("/paste/", pastePOSTWebInterface)
// Show public paste.
c.Echo.GET("/paste/:id", pasteGET)
ctx.Echo.GET("/paste/:id", pasteGETWebInterface)
// Show RAW representation of public paste.
c.Echo.GET("/paste/:id/raw", pasteRawGET)
ctx.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface)
// Show private paste.
c.Echo.GET("/paste/:id/:timestamp", pasteGET)
ctx.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface)
// Show RAW representation of private paste.
c.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGET)
ctx.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGETWebInterface)
// Verify access to passworded paste.
c.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
c.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
ctx.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
ctx.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
// Pastes list.
c.Echo.GET("/pastes/", pastesGET)
c.Echo.GET("/pastes/:page", pastesGET)
ctx.Echo.GET("/pastes/", pastesGET)
ctx.Echo.GET("/pastes/:page", pastesGET)
}

356
domains/pastes/paste_get.go Normal file
View File

@ -0,0 +1,356 @@
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)
}

View File

@ -0,0 +1,162 @@
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)
}

View File

@ -0,0 +1,111 @@
// 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)
}

View File

@ -1,6 +1,13 @@
# Database configuration.
# Only MySQL database is supported for now.
# Only MySQL database and flatfiles are 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"
@ -18,9 +25,16 @@ logging:
# HTTP server configuration.
http:
address: "localhost"
address: "127.0.0.1"
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: false
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

View File

@ -1,111 +0,0 @@
# 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/footer.html"
- "assets/index.html"
- "assets/main.html"
- "assets/navigation.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 Normal file
View File

@ -0,0 +1,23 @@
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 Normal file
View File

@ -0,0 +1,91 @@
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=

View File

@ -25,31 +25,32 @@
package captcha
import (
"github.com/labstack/echo"
// local
"github.com/pztrn/fastpastebin/context"
// other
"github.com/dchest/captcha"
"github.com/labstack/echo"
"github.com/rs/zerolog"
"go.dev.pztrn.name/fastpastebin/internal/context"
)
var (
c *context.Context
ctx *context.Context
log zerolog.Logger
)
// New initializes captcha package and adds neccessary HTTP and API
// New initializes captcha package and adds necessary HTTP and API
// endpoints.
func New(cc *context.Context) {
c = cc
ctx = cc
log = ctx.Logger.With().Str("type", "internal").Str("package", "captcha").Logger()
// New paste.
c.Echo.GET("/captcha/:id.png", echo.WrapHandler(captcha.Server(captcha.StdWidth, captcha.StdHeight)))
ctx.Echo.GET("/captcha/:id.png", echo.WrapHandler(captcha.Server(captcha.StdWidth, captcha.StdHeight)))
}
// NewCaptcha creates new captcha string.
func NewCaptcha() string {
s := captcha.New()
c.Logger.Debug().Msgf("Created new captcha string: %s", s)
log.Debug().Str("captcha string", s).Msg("Created new captcha string")
return s
}

View File

@ -24,8 +24,10 @@
package config
// ConfigDatabase describes database configuration.
type ConfigDatabase struct {
// Database describes database configuration.
type Database struct {
Type string `yaml:"type"`
Path string `yaml:"path"`
Address string `yaml:"address"`
Port string `yaml:"port"`
Username string `yaml:"username"`

33
internal/config/http.go Normal file
View File

@ -0,0 +1,33 @@
// 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 config
// HTTP describes HTTP server configuration.
type HTTP struct {
Address string `yaml:"address"`
Port string `yaml:"port"`
MaxBodySizeMegabytes string `yaml:"max_body_size_megabytes"`
AllowInsecure bool `yaml:"allow_insecure"`
}

View File

@ -24,9 +24,9 @@
package config
// ConfigLogging describes logger configuration.
type ConfigLogging struct {
LogToFile bool `yaml:"log_to_file"`
// Logging describes logger configuration.
type Logging struct {
FileName string `yaml:"filename"`
LogLevel string `yaml:"loglevel"`
LogToFile bool `yaml:"log_to_file"`
}

View File

@ -24,9 +24,7 @@
package config
// ConfigHTTP describes HTTP server configuration.
type ConfigHTTP struct {
Address string `yaml:"address"`
Port string `yaml:"port"`
AllowInsecure bool `yaml:"allow_insecure"`
// Pastes describes pastes subsystem configuration.
type Pastes struct {
Pagination int `yaml:"pagination"`
}

View File

@ -24,9 +24,10 @@
package config
// ConfigStruct describes whole configuration.
type ConfigStruct struct {
Database ConfigDatabase `yaml:"database"`
Logging ConfigLogging `yaml:"logging"`
HTTP ConfigHTTP `yaml:"http"`
// Struct describes whole configuration.
type Struct struct {
Database Database `yaml:"database"`
Logging Logging `yaml:"logging"`
HTTP HTTP `yaml:"http"`
Pastes Pastes `yaml:"pastes"`
}

View File

@ -25,19 +25,14 @@
package context
import (
// stdlib
"io/ioutil"
"flag"
"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"
)
@ -46,26 +41,24 @@ import (
// contains everything every part of application need, like configuration
// access, logger, etc.
type Context struct {
Config *config.ConfigStruct
Database databaseinterface.Interface
Echo *echo.Echo
Flagger *flagger.Flagger
Logger zerolog.Logger
Config *config.Struct
Database databaseinterface.Interface
Echo *echo.Echo
Logger zerolog.Logger
configPathFromCLI string
}
// Initialize initializes context.
func (c *Context) Initialize() {
c.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
c.initializeLogger()
c.Flagger = flagger.New(nil)
c.Flagger.Initialize()
flag.StringVar(&c.configPathFromCLI, "config", "NO_CONFIG", "Configuration file path. Can be overridded with FASTPASTEBIN_CONFIG environment variable.")
}
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",
})
// InitializePost initializes everything that needs a configuration.
func (c *Context) InitializePost() {
c.initializeLoggerPost()
c.initializeHTTPServer()
}
// LoadConfiguration loads configuration and executes right after Flagger
@ -74,63 +67,45 @@ func (c *Context) Initialize() {
func (c *Context) LoadConfiguration() {
c.Logger.Info().Msg("Loading configuration...")
var configPath = ""
configPath := c.configPathFromCLI
// 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 err != nil && configPathFromEnvFound || err == nil && configPathFromEnvFound {
if configPathFromEnvFound {
configPath = configPathFromEnv
} else if err != nil && !configPathFromEnvFound || err == nil && configPathFromCLI == "NO_CONFIG" {
}
if configPath == "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().Msgf("Failed to normalize path to configuration file: %s", err1.Error())
c.Logger.Fatal().Err(err1).Msg("Failed to normalize path to configuration file")
}
c.Logger.Debug().Msgf("Configuration file path: %s", configPath)
c.Logger.Debug().Str("path", configPath).Msg("Configuration file path")
c.Config = &config.ConfigStruct{}
//nolint:exhaustruct
c.Config = &config.Struct{}
// Read configuration file.
fileData, err2 := ioutil.ReadFile(normalizedConfigPath)
fileData, err2 := os.ReadFile(normalizedConfigPath)
if err2 != nil {
c.Logger.Panic().Msgf("Failed to read configuration file: %s", err2.Error())
c.Logger.Panic().Err(err2).Msg("Failed to read configuration file")
}
// Parse it into structure.
err3 := yaml.Unmarshal(fileData, c.Config)
if err3 != nil {
c.Logger.Panic().Msgf("Failed to parse configuration file: %s", err3.Error())
c.Logger.Panic().Err(err3).Msg("Failed to parse configuration file")
}
// Yay! See what it gets!
c.Logger.Debug().Msgf("Parsed configuration: %+v", c.Config)
// 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)
}
}
// RegisterDatabaseInterface registers database interface for later use.
@ -146,4 +121,6 @@ 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()
}

View File

@ -26,10 +26,11 @@ package context
const (
// Version .
Version = "0.1.1"
Version = "0.4.1"
)
// New creates new context.
func New() *Context {
//nolint:exhaustruct
return &Context{}
}

View File

@ -0,0 +1,46 @@
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)
}
}
}

View File

@ -0,0 +1,79 @@
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)
}
}

View File

@ -0,0 +1,137 @@
// 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()
}

View File

@ -0,0 +1,45 @@
// 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 (
"go.dev.pztrn.name/fastpastebin/internal/context"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
)
const FlatFileDialect = "flatfiles"
var (
ctx *context.Context
flf *FlatFiles
)
func New(cc *context.Context) {
ctx = cc
//nolint:exhaustruct
flf = &FlatFiles{}
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
}

View File

@ -0,0 +1,304 @@
// 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
}
}

View File

@ -0,0 +1,65 @@
// 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()
}

View File

@ -0,0 +1,31 @@
// 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
// Index describes paste index structure for flatfiles data storage.
type Index struct {
ID int `yaml:"id"`
Private bool `json:"private"`
}

View File

@ -0,0 +1,42 @@
// 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()
}

View File

@ -0,0 +1,43 @@
// 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 (
"go.dev.pztrn.name/fastpastebin/internal/context"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
)
var (
ctx *context.Context
dbAdapter *Database
)
func New(cc *context.Context) {
ctx = cc
//nolint:exhaustruct
dbAdapter = &Database{}
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
}

View File

@ -0,0 +1,65 @@
// 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()
}

View File

@ -25,13 +25,20 @@
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
}

View File

@ -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,6 +41,7 @@ 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
}

View File

@ -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,6 +41,7 @@ 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
}

View File

@ -25,32 +25,35 @@
package migrations
import (
// stdlib
"database/sql"
)
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).'")
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).'")
if err != nil {
//nolint:wrapcheck
return err
}
_, err1 := tx.Exec("ALTER TABLE `pastes` ADD `password_salt` varchar(64) NOT NULL DEFAULT '' COMMENT 'Password salt (sha256ed).'")
_, err1 := txn.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(tx *sql.Tx) error {
_, err := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `password`")
func PasswordedPastesDown(txn *sql.Tx) error {
_, err := txn.Exec("ALTER TABLE `pastes` DROP COLUMN `password`")
if err != nil {
//nolint:wrapcheck
return err
}
_, err1 := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `password_salt`")
_, err1 := txn.Exec("ALTER TABLE `pastes` DROP COLUMN `password_salt`")
if err1 != nil {
//nolint:wrapcheck
return err1
}

View File

@ -25,37 +25,35 @@
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 (
c *context.Context
)
var ctx *context.Context
// New initializes migrations.
func New(cc *context.Context) {
c = cc
ctx = cc
}
// Migrate launching migrations.
func Migrate() {
c.Logger.Info().Msg("Migrating database...")
ctx.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 := 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())
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")
}
}

View File

@ -0,0 +1,214 @@
// 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")
}
}
}

View File

@ -0,0 +1,43 @@
// 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 (
"go.dev.pztrn.name/fastpastebin/internal/context"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
)
var (
ctx *context.Context
dbAdapter *Database
)
func New(cc *context.Context) {
ctx = cc
//nolint:exhaustruct
dbAdapter = &Database{}
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
}

View File

@ -0,0 +1,65 @@
// 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()
}

View File

@ -0,0 +1,56 @@
// 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
}

View File

@ -22,23 +22,28 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package database
package migrations
import (
// other
"github.com/jmoiron/sqlx"
"database/sql"
)
// Handler is an interfaceable structure that proxifies calls from anyone
// to Database structure.
type Handler struct{}
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
}
// GetDatabaseConnection returns current database connection.
func (dbh Handler) GetDatabaseConnection() *sqlx.DB {
return d.GetDatabaseConnection()
return nil
}
// Initialize initializes connection to database.
func (dbh Handler) Initialize() {
d.Initialize()
func PasteLangDown(tx *sql.Tx) error {
_, err := tx.Exec("ALTER TABLE pastes DROP COLUMN language")
if err != nil {
//nolint:wrapcheck
return err
}
return nil
}

View File

@ -0,0 +1,49 @@
// 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
}

View File

@ -0,0 +1,61 @@
// 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
}

View File

@ -22,49 +22,39 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package api
package migrations
import (
// 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"
"github.com/pressly/goose"
"go.dev.pztrn.name/fastpastebin/internal/context"
)
var (
c *context.Context
e *echo.Echo
)
var ctx *context.Context
// New initializes variables for api package.
// New initializes migrations.
func New(cc *context.Context) {
c = cc
ctx = cc
}
// InitializeAPI initializes HTTP API and starts web server.
func InitializeAPI() {
c.Logger.Info().Msg("Initializing HTTP server...")
// Migrate launching migrations.
func Migrate() {
ctx.Logger.Info().Msg("Migrating database...")
e = echo.New()
e.Use(echoReqLogger())
e.Use(middleware.Recover())
e.DisableHTTP2 = true
e.HideBanner = true
e.HidePort = true
_ = 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.
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)
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")
}
}

View File

@ -0,0 +1,228 @@
// 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")
}
}
}

View File

@ -25,19 +25,20 @@
package database
import (
// local
"github.com/pztrn/fastpastebin/context"
"github.com/pztrn/fastpastebin/database/interface"
"go.dev.pztrn.name/fastpastebin/internal/context"
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
)
var (
c *context.Context
d *Database
ctx *context.Context
dbAdapter *Database
)
// New initializes database structure.
func New(cc *context.Context) {
c = cc
d = &Database{}
c.RegisterDatabaseInterface(databaseinterface.Interface(Handler{}))
ctx = cc
//nolint:exhaustruct
dbAdapter = &Database{}
ctx.RegisterDatabaseInterface(databaseinterface.Interface(Handler{}))
}

View File

@ -0,0 +1,73 @@
// 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()
}

View File

@ -25,13 +25,22 @@
package databaseinterface
import (
// other
"github.com/jmoiron/sqlx"
"database/sql"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
"go.dev.pztrn.name/fastpastebin/internal/structs"
)
// Interface represents database interface which is available to all
// parts of application and registers with context.Context.
type Interface interface {
GetDatabaseConnection() *sqlx.DB
DeletePaste(int) error
GetDatabaseConnection() *sql.DB
GetPaste(pasteID int) (*structs.Paste, error)
GetPagedPastes(page int) ([]structs.Paste, error)
GetPastesPages() int
Initialize()
RegisterDialect(dialectinterface.Interface)
SavePaste(p *structs.Paste) (int64, error)
Shutdown()
}

View File

@ -1,56 +1,61 @@
package pagination
import (
// stdlib
"strconv"
"strings"
// local
"github.com/pztrn/fastpastebin/api/http/static"
"go.dev.pztrn.name/fastpastebin/assets"
)
// CreateHTML creates pagination HTML based on passed parameters.
func CreateHTML(currentPage int, pages int, linksBase string) string {
// Load templates.
paginationHTMLRaw, err := static.ReadFile("pagination.html")
paginationHTMLRaw, err := assets.Data.ReadFile("pagination.html")
if err != nil {
return "Missing pagination.html"
}
paginationLinkRaw, err1 := static.ReadFile("pagination_link.html")
paginationLinkRaw, err1 := assets.Data.ReadFile("pagination_link.html")
if err1 != nil {
return "Missing pagination_link.html"
}
paginationLinkCurrentRaw, err2 := static.ReadFile("pagination_link_current.html")
paginationLinkCurrentRaw, err2 := assets.Data.ReadFile("pagination_link_current.html")
if err2 != nil {
return "Missing pagination_link_current.html"
}
paginationEllipsisRaw, err3 := static.ReadFile("pagination_ellipsis.html")
paginationEllipsisRaw, err3 := assets.Data.ReadFile("pagination_ellipsis.html")
if err3 != nil {
return "Missing pagination_ellipsis.html"
}
// First page should always be visible.
var paginationString = ""
var paginationString string
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(string(paginationString), "{paginationLink}", linksBase+"1", -1)
paginationString = strings.Replace(paginationString, "{paginationLink}", linksBase+"1", -1)
}
var ellipsisStartAdded = false
var ellipsisEndAdded = false
i := 2
var (
ellipsisStartAdded = false
ellipsisEndAdded = false
//nolint:varnamelen
i = 2
)
for i <= pages {
// ToDo: fix it!
//nolint:nestif
if pages > 5 {
if currentPage-3 < i && currentPage+3 > i || i == pages {
var paginationItemRaw = string(paginationLinkRaw)
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
@ -64,7 +69,7 @@ func CreateHTML(currentPage int, pages int, linksBase string) string {
}
}
} else {
var paginationItemRaw = string(paginationLinkRaw)
paginationItemRaw := string(paginationLinkRaw)
if i == currentPage {
paginationItemRaw = string(paginationLinkCurrentRaw)
}
@ -73,7 +78,7 @@ func CreateHTML(currentPage int, pages int, linksBase string) string {
paginationString += paginationItem
}
i += 1
i++
}
pagination := strings.Replace(string(paginationHTMLRaw), "{paginationLinks}", paginationString, 1)

View File

@ -22,56 +22,64 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package pastes
package structs
import (
// stdlib
"crypto/sha256"
"fmt"
"math/rand"
"time"
// other
"golang.org/x/crypto/scrypt"
)
const (
PASTE_KEEP_FOR_MINUTES = 1
PASTE_KEEP_FOR_HOURS = 2
PASTE_KEEP_FOR_DAYS = 3
PASTE_KEEP_FOR_MONTHS = 4
// 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
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
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,
}
)
// 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,
}
// Paste represents paste itself.
type Paste struct {
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"`
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"`
}
// 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))]
}
@ -82,8 +90,10 @@ 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)
@ -93,19 +103,23 @@ 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 PASTE_KEEP_FOR_MINUTES:
case PasteKeepForever:
expirationTime = time.Now().UTC().Add(time.Hour * 1)
case PasteKeepForMinutes:
expirationTime = p.CreatedAt.Add(time.Minute * time.Duration(p.KeepFor))
case PASTE_KEEP_FOR_HOURS:
case PasteKeepForHours:
expirationTime = p.CreatedAt.Add(time.Hour * time.Duration(p.KeepFor))
case PASTE_KEEP_FOR_DAYS:
case PasteKeepForDays:
expirationTime = p.CreatedAt.Add(time.Hour * 24 * time.Duration(p.KeepFor))
case PASTE_KEEP_FOR_MONTHS:
case PasteKeepForMonths:
expirationTime = p.CreatedAt.Add(time.Hour * 24 * 30 * time.Duration(p.KeepFor))
}
@ -117,11 +131,7 @@ func (p *Paste) IsExpired() bool {
curTime := time.Now().UTC()
expirationTime := p.GetExpirationTime()
if curTime.Sub(expirationTime).Seconds() > 0 {
return true
}
return false
return curTime.Sub(expirationTime).Seconds() > 0
}
// VerifyPassword verifies that provided password is valid.
@ -131,12 +141,9 @@ func (p *Paste) VerifyPassword(password string) bool {
if err != nil {
return false
}
passwordHashBytes := sha256.Sum256(passwordCrypted)
providedPassword := fmt.Sprintf("%x", passwordHashBytes)
if providedPassword == p.Password {
return true
}
return false
return providedPassword == p.Password
}

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