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