Compare commits
No commits in common. "master" and "v0.2.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
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,7 +1 @@
|
||||||
examples/fastpastebin.yaml
|
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
|
|
||||||
}
|
|
84
CHANGELOG.md
84
CHANGELOG.md
|
@ -1,84 +1,12 @@
|
||||||
# Changelog
|
# 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.1.0
|
||||||
|
|
||||||
## [0.4.1] - 2022-08-14
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
* Update docker images - alpine to 3.16.1, golang to 1.19, golangci-lint to 1.48.0.
|
|
||||||
* Update chroma to v2.2.0.
|
|
||||||
* Update bulma to 0.9.4.
|
|
||||||
* Update bulma-tooltip to 1.2 (was 3.0.0, but repo was switched to CreativeBulma).
|
|
||||||
* Update github.com/dchest/captcha to v1.0.0.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
* Put valid repository's link in footer.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
* Removed `flagger` dependency.
|
|
||||||
|
|
||||||
## [0.4.0] - 2021-01-09
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
* PostgreSQL support.
|
|
||||||
* Docker containerization for every commit and tag.
|
|
||||||
* Pastes cleanup procedure.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
* Updated bulma to v 0.7.5.
|
|
||||||
* Moved from `io/ioutil` to `os` package for reading/writing files and directories.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
* Dirty hack to get database connection reestablish (for sure).
|
|
||||||
|
|
||||||
## 0.3.0
|
|
||||||
|
|
||||||
Release changelogs lost :(.
|
|
||||||
|
|
||||||
## [0.2.0] - 2018-05-27
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
* Possibility to create different database backends. Currently `mysql` and `flatfiles` are available.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
* De-hardcoded pagination configuration, it is now configurable via configuration file.
|
|
||||||
|
|
||||||
## [0.1.1] - 2018-05-26
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
* Footer copyrights.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
* Refactored templates: now they're included in each other if neccessary.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
* Fixed nasty bugs with private pastes that causing fastpastebin to crash.
|
|
||||||
* Logger level from configuration now properly set.
|
|
||||||
|
|
||||||
## [0.1.0] - 2018-05-19
|
|
||||||
|
|
||||||
First normal release. Fast Paste Bin is able to handle public, private
|
First normal release. Fast Paste Bin is able to handle public, private
|
||||||
and passworded pastes.
|
and passworded pastes.
|
||||||
|
|
||||||
[Unreleased]: https://code.pztrn.name/apps/fastpastebin/compare/v0.4.1...HEAD
|
|
||||||
[0.4.1]: https://code.pztrn.name/apps/fastpastebin/compare/0.4.0...v0.4.1
|
|
||||||
[0.4.0]: https://code.pztrn.name/apps/fastpastebin/compare/v0.2.0...0.4.0
|
|
||||||
[0.2.0]: https://code.pztrn.name/apps/fastpastebin/compare/v0.1.1...v0.2.0
|
|
||||||
[0.1.1]: https://code.pztrn.name/apps/fastpastebin/compare/v0.1.0...v0.1.1
|
|
||||||
[0.1.0]: https://code.pztrn.name/apps/fastpastebin/src/tag/v0.1.0
|
|
17
Dockerfile
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
|
103
README.md
103
README.md
|
@ -1,90 +1,79 @@
|
||||||
|
[Chat on Keybase.io](https://keybase.io/team/fastpastebin)
|
||||||
|
|
||||||
# Fast Pastebin
|
# 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!
|
# Current functionality.
|
||||||
|
|
||||||
**Please, use [my gitea](https://code.pztrn.name/apps/fastpastebin) for bug reporting. All other places are mirrors!**
|
|
||||||
|
|
||||||
Also, [join Matrix room](https://matrix.to/#/%23fastpastebin:pztrn.online?via=matrix.org) for near-realtime chat.
|
|
||||||
|
|
||||||
## Current functionality
|
|
||||||
|
|
||||||
* Create and view public and private pastes.
|
* Create and view public and private pastes.
|
||||||
* Syntax highlighting.
|
* Syntax highlighting.
|
||||||
* Pastes expiration.
|
* Pastes expiration.
|
||||||
* Passwords for pastes.
|
* Passwords for pastes.
|
||||||
* Multiple storage backends. Currently: ``flatfiles``, ``mysql`` and ``postgresql``.
|
* Multiple storage backends. Currently: ``flatfiles`` and ``mysql``.
|
||||||
|
|
||||||
## 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:
|
Just issue:
|
||||||
|
|
||||||
```bash
|
|
||||||
CGO_ENABLED=0 go install go.dev.pztrn.name/fastpastebin/cmd/fastpastebin@VERSION
|
|
||||||
```
|
```
|
||||||
|
go get -u -v github.com/pztrn/fastpastebin/cmd/fastpastebin
|
||||||
Replace `VERSION` with a [tag of your choice](https://code.pztrn.name/apps/fastpastebin/releases).
|
```
|
||||||
|
|
||||||
This command can be used to update Fast Paste Bin.
|
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
|
Take a look at [example configuration file](examples/fastpastebin.yaml.dist)
|
||||||
---
|
which contains all supported options and their descriptions.
|
||||||
version: "2.4"
|
|
||||||
|
|
||||||
services:
|
Configuration file position is irrelevant, there is no hardcoded paths where
|
||||||
fastpastebin:
|
Fast Paste Bin looking for it's configuration. Use ``-config`` CLI parameter
|
||||||
restart: always
|
or ``FASTPASTEBIN_CONFIG`` environment variable to specify path.
|
||||||
image: code.pztrn.name/apps/fastpastebin:0.4.1
|
|
||||||
volumes:
|
# Developing
|
||||||
- "./fastpastebin.yaml:/app/fastpastebin.yaml"
|
|
||||||
ports:
|
Developers should install https://github.com/UnnoTed/fileb0x/ which is used
|
||||||
- "25544:25544"
|
as replacement to go-bindata for embedding assets into binary. After changing
|
||||||
cpus: 2
|
assets they should be recompiled into Go code. At repository root execute
|
||||||
mem_limit: 1G
|
this command and you'll be fine:
|
||||||
memswap_limit: 0
|
|
||||||
|
```
|
||||||
|
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**.
|
This is a ToDo list which isn't sorted by any parameter at all. Just a list
|
||||||
|
of tasks you can help with.
|
||||||
Branch `master` represents "latest version" state and always stable.
|
|
||||||
|
|
||||||
### Code
|
|
||||||
|
|
||||||
Use linters, formatters, etc. VSCode with Go plugin is recommended for developing as it will perform most of linting-formatting
|
|
||||||
actions automagically.
|
|
||||||
|
|
||||||
Also, Sublime Text with LSP-gopls will also work just fine.
|
|
||||||
|
|
||||||
Try to follow [Go's code review comments](https://github.com/golang/go/wiki/CodeReviewComments) with few exceptions:
|
|
||||||
|
|
||||||
* We're not forcing any limits on line length for code, only for comments, they should be 72-76 chars long.
|
|
||||||
|
|
||||||
## ToDo
|
|
||||||
|
|
||||||
This is a ToDo list which isn't sorted by any parameter at all. Just a list of tasks you can help with.
|
|
||||||
|
|
||||||
* User CP.
|
* User CP.
|
||||||
* Files uploading.
|
* Files uploading.
|
||||||
* Passwords for files.
|
* Passwords for files.
|
||||||
* Pastes forking and revisioning (like git or github gists).
|
* 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.
|
* CLI client for pastes and files uploading.
|
|
@ -22,24 +22,32 @@
|
||||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
package dbnotavailable
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
// other
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/templater"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database not available error page.
|
// Logs Echo requests.
|
||||||
func dbNotAvailableGet(ec echo.Context) error {
|
func echoReqLog(ec echo.Context, next echo.HandlerFunc) error {
|
||||||
htmlData := templater.GetTemplate(ec, "database_not_available.html", nil)
|
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
|
next(ec)
|
||||||
return ec.HTML(http.StatusInternalServerError, htmlData)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbNotAvailableRawGet(ec echo.Context) error {
|
// Wrapper around previous function.
|
||||||
//nolint:wrapcheck
|
func echoReqLogger() echo.MiddlewareFunc {
|
||||||
return ec.String(http.StatusInternalServerError, "Database not available\nSomething went wrong while trying to connect to database. Check logs for details.")
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return echoReqLog(c, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
70
api/exported.go
Normal file
70
api/exported.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// 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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/api/http"
|
||||||
|
"github.com/pztrn/fastpastebin/api/json"
|
||||||
|
"github.com/pztrn/fastpastebin/context"
|
||||||
|
|
||||||
|
// other
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
"github.com/labstack/echo/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
c *context.Context
|
||||||
|
e *echo.Echo
|
||||||
|
)
|
||||||
|
|
||||||
|
// New initializes variables for api package.
|
||||||
|
func New(cc *context.Context) {
|
||||||
|
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
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
package flatfiles
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
// local
|
||||||
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
|
"github.com/pztrn/fastpastebin/api/http/static"
|
||||||
)
|
"github.com/pztrn/fastpastebin/context"
|
||||||
|
|
||||||
const FlatFileDialect = "flatfiles"
|
// other
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx *context.Context
|
c *context.Context
|
||||||
flf *FlatFiles
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// New initializes basic HTTP API, which shows only index page and serves
|
||||||
|
// static files.
|
||||||
func New(cc *context.Context) {
|
func New(cc *context.Context) {
|
||||||
ctx = cc
|
c = cc
|
||||||
//nolint:exhaustruct
|
c.Logger.Info().Msg("Initializing HTTP API...")
|
||||||
flf = &FlatFiles{}
|
|
||||||
|
|
||||||
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
|
// Static files.
|
||||||
|
c.Echo.GET("/static/*", echo.WrapHandler(static.Handler))
|
||||||
|
|
||||||
|
// Index.
|
||||||
|
c.Echo.GET("/", indexGet)
|
||||||
}
|
}
|
|
@ -22,31 +22,27 @@
|
||||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
package indexpage
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/alecthomas/chroma/v2/lexers"
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/captcha"
|
||||||
|
"github.com/pztrn/fastpastebin/templater"
|
||||||
|
|
||||||
|
// other
|
||||||
|
"github.com/alecthomas/chroma/lexers"
|
||||||
"github.com/labstack/echo"
|
"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.
|
// Index of this site.
|
||||||
func indexGet(ectx echo.Context) error {
|
func indexGet(ec echo.Context) error {
|
||||||
// We should check if database connection available.
|
|
||||||
dbConn := ctx.Database.GetDatabaseConnection()
|
|
||||||
if ctx.Config.Database.Type != flatfiles.FlatFileDialect && dbConn == nil {
|
|
||||||
//nolint:wrapcheck
|
|
||||||
return ectx.Redirect(http.StatusFound, "/database_not_available")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate list of available languages to highlight.
|
// Generate list of available languages to highlight.
|
||||||
availableLexers := lexers.Names(false)
|
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 {
|
for i := range availableLexers {
|
||||||
availableLexersSelectOpts += "<option value='" + availableLexers[i] + "'>" + availableLexers[i] + "</option>"
|
availableLexersSelectOpts += "<option value='" + availableLexers[i] + "'>" + availableLexers[i] + "</option>"
|
||||||
}
|
}
|
||||||
|
@ -54,8 +50,7 @@ func indexGet(ectx echo.Context) error {
|
||||||
// Captcha.
|
// Captcha.
|
||||||
captchaString := captcha.NewCaptcha()
|
captchaString := captcha.NewCaptcha()
|
||||||
|
|
||||||
htmlData := templater.GetTemplate(ectx, "index.html", map[string]string{"lexers": availableLexersSelectOpts, "captchaString": captchaString})
|
htmlData := templater.GetTemplate(ec, "index.html", map[string]string{"lexers": availableLexersSelectOpts, "captchaString": captchaString})
|
||||||
|
|
||||||
//nolint:wrapcheck
|
return ec.HTML(http.StatusOK, htmlData)
|
||||||
return ectx.HTML(http.StatusOK, htmlData)
|
|
||||||
}
|
}
|
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-26 13:58:30.612342807 +0500 +05 m=+0.013490397" from config file "fileb0x.yml" DO NOT EDIT.
|
||||||
|
// modification hash(e9e210106a4d77680942064c40d06a3e.522a9cc525fa984df98098d4c3992752)
|
||||||
|
|
||||||
|
package static
|
||||||
|
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
|
||||||
|
"golang.org/x/net/webdav"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// CTX is a context for webdav vfs
|
||||||
|
CTX = context.Background()
|
||||||
|
|
||||||
|
|
||||||
|
// FS is a virtual memory file system
|
||||||
|
FS = webdav.NewMemFS()
|
||||||
|
|
||||||
|
|
||||||
|
// Handler is used to server files through a http handler
|
||||||
|
Handler *webdav.Handler
|
||||||
|
|
||||||
|
// HTTP is the http file system
|
||||||
|
HTTP http.FileSystem = new(HTTPFS)
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPFS implements http.FileSystem
|
||||||
|
type HTTPFS struct {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if CTX.Err() != nil {
|
||||||
|
panic(CTX.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
err = FS.Mkdir(CTX, "static/", 0777)
|
||||||
|
if err != nil && err != os.ErrExist {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
err = FS.Mkdir(CTX, "static/css/", 0777)
|
||||||
|
if err != nil && err != os.ErrExist {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
err = FS.Mkdir(CTX, "static/js/", 0777)
|
||||||
|
if err != nil && err != os.ErrExist {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Handler = &webdav.Handler{
|
||||||
|
FileSystem: FS,
|
||||||
|
LockSystem: webdav.NewMemLS(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Open a file
|
||||||
|
func (hfs *HTTPFS) Open(path string) (http.File, error) {
|
||||||
|
|
||||||
|
|
||||||
|
f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile is adapTed from ioutil
|
||||||
|
func ReadFile(path string) ([]byte, error) {
|
||||||
|
f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, bytes.MinRead))
|
||||||
|
|
||||||
|
// If the buffer overflows, we will get bytes.ErrTooLarge.
|
||||||
|
// Return that as an error. Any other panic remains.
|
||||||
|
defer func() {
|
||||||
|
e := recover()
|
||||||
|
if e == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
|
||||||
|
err = panicErr
|
||||||
|
} else {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err = buf.ReadFrom(f)
|
||||||
|
return buf.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile is adapTed from ioutil
|
||||||
|
func WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||||
|
f, err := FS.OpenFile(CTX, filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n, err := f.Write(data)
|
||||||
|
if err == nil && n < len(data) {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
}
|
||||||
|
if err1 := f.Close(); err == nil {
|
||||||
|
err = err1
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalkDirs looks for files in the given dir and returns a list of files in it
|
||||||
|
// usage for all files in the b0x: WalkDirs("", false)
|
||||||
|
func WalkDirs(name string, includeDirsInList bool, files ...string) ([]string, error) {
|
||||||
|
f, err := FS.OpenFile(CTX, name, os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfos, err := f.Readdir(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, info := range fileInfos {
|
||||||
|
filename := path.Join(name, info.Name())
|
||||||
|
|
||||||
|
if includeDirsInList || !info.IsDir() {
|
||||||
|
files = append(files, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
files, err = WalkDirs(filename, includeDirsInList, files...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
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-26 13:29:06.05698436 +0500 +05 m=+0.013970976" from config file "fileb0x.yml" DO NOT EDIT.
|
||||||
|
// modified(2018-05-26 13:29:04.002355958 +0500 +05)
|
||||||
|
// original path: assets/error.html
|
||||||
|
|
||||||
|
package static
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileErrorHTML is "/error.html"
|
||||||
|
var FileErrorHTML = []byte("\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x62\x6f\x78\x20\x68\x61\x73\x2d\x62\x61\x63\x6b\x67\x72\x6f\x75\x6e\x64\x2d\x77\x61\x72\x6e\x69\x6e\x67\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x3e\x7b\x65\x72\x72\x6f\x72\x7d\x3c\x2f\x70\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
|
||||||
|
f, err := FS.OpenFile(CTX, "/error.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_, err = f.Write(FileErrorHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
35
api/http/static/b0xfile__footer.html.go
Normal file
35
api/http/static/b0xfile__footer.html.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Code generaTed by fileb0x at "2018-05-26 12:55:42.087956217 +0500 +05 m=+0.009637095" from config file "fileb0x.yml" DO NOT EDIT.
|
||||||
|
// modified(2018-05-26 12:54:22.9990735 +0500 +05)
|
||||||
|
// original path: assets/footer.html
|
||||||
|
|
||||||
|
package static
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileFooterHTML is "/footer.html"
|
||||||
|
var FileFooterHTML = []byte("\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6f\x6e\x74\x61\x69\x6e\x65\x72\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6f\x6e\x74\x65\x6e\x74\x20\x68\x61\x73\x2d\x74\x65\x78\x74\x2d\x63\x65\x6e\x74\x65\x72\x65\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x74\x72\x6f\x6e\x67\x3e\x46\x61\x73\x74\x20\x70\x61\x73\x74\x65\x20\x62\x69\x6e\x3c\x2f\x73\x74\x72\x6f\x6e\x67\x3e\x20\x76\x65\x72\x73\x69\x6f\x6e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x74\x72\x6f\x6e\x67\x3e\x7b\x76\x65\x72\x73\x69\x6f\x6e\x7d\x3c\x2f\x73\x74\x72\x6f\x6e\x67\x3e\x20\x62\x79\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x70\x7a\x74\x72\x6e\x2e\x6e\x61\x6d\x65\x22\x3e\x53\x74\x61\x6e\x69\x73\x6c\x61\x76\x20\x4e\x2e\x20\x61\x6b\x61\x20\x70\x7a\x74\x72\x6e\x3c\x2f\x61\x3e\x2e\x20\x54\x68\x65\x20\x73\x6f\x75\x72\x63\x65\x20\x63\x6f\x64\x65\x20\x69\x73\x20\x6c\x69\x63\x65\x6e\x73\x65\x64\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6f\x70\x65\x6e\x73\x6f\x75\x72\x63\x65\x2e\x6f\x72\x67\x2f\x6c\x69\x63\x65\x6e\x73\x65\x73\x2f\x6d\x69\x74\x2d\x6c\x69\x63\x65\x6e\x73\x65\x2e\x70\x68\x70\x22\x3e\x4d\x49\x54\x3c\x2f\x61\x3e\x2e\x20\x47\x65\x74\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x67\x69\x74\x68\x75\x62\x2e\x63\x6f\x6d\x2f\x70\x7a\x74\x72\x6e\x2f\x66\x61\x73\x74\x70\x61\x73\x74\x65\x62\x69\x6e\x22\x3e\x73\x6f\x75\x72\x63\x65\x20\x6f\x72\x20\x62\x69\x6e\x61\x72\x79\x20\x72\x65\x6c\x65\x61\x73\x65\x73\x20\x68\x65\x72\x65\x3c\x2f\x61\x3e\x21\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x70\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x64\x69\x76\x3e")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
|
||||||
|
f, err := FS.OpenFile(CTX, "/footer.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_, err = f.Write(FileFooterHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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__main.html.go
Normal file
35
api/http/static/b0xfile__main.html.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Code generaTed by fileb0x at "2018-05-26 12:42:22.452857215 +0500 +05 m=+0.054711585" from config file "fileb0x.yml" DO NOT EDIT.
|
||||||
|
// modified(2018-05-26 12:41:36.015758918 +0500 +05)
|
||||||
|
// original path: assets/main.html
|
||||||
|
|
||||||
|
package static
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileMainHTML is "/main.html"
|
||||||
|
var FileMainHTML = []byte("\x3c\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x74\x6d\x6c\x3e\x0a\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\x66\x2d\x38\x22\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x6e\x61\x6d\x65\x3d\x22\x76\x69\x65\x77\x70\x6f\x72\x74\x22\x20\x63\x6f\x6e\x74\x65\x6e\x74\x3d\x22\x77\x69\x64\x74\x68\x3d\x64\x65\x76\x69\x63\x65\x2d\x77\x69\x64\x74\x68\x2c\x20\x69\x6e\x69\x74\x69\x61\x6c\x2d\x73\x63\x61\x6c\x65\x3d\x31\x22\x3e\x0a\x20\x20\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x62\x75\x6c\x6d\x61\x2d\x30\x2e\x37\x2e\x30\x2e\x6d\x69\x6e\x2e\x63\x73\x73\x22\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x62\x75\x6c\x6d\x61\x2d\x74\x6f\x6f\x6c\x74\x69\x70\x2d\x31\x2e\x30\x2e\x34\x2e\x6d\x69\x6e\x2e\x63\x73\x73\x22\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x73\x74\x79\x6c\x65\x2e\x63\x73\x73\x22\x3e\x0a\x3c\x2f\x68\x65\x61\x64\x3e\x0a\x0a\x3c\x62\x6f\x64\x79\x3e\x0a\x20\x20\x20\x20\x7b\x6e\x61\x76\x69\x67\x61\x74\x69\x6f\x6e\x7d\x20\x7b\x64\x6f\x63\x75\x6d\x65\x6e\x74\x42\x6f\x64\x79\x7d\x0a\x3c\x2f\x62\x6f\x64\x79\x3e\x0a\x3c\x66\x6f\x6f\x74\x65\x72\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x6f\x6f\x74\x65\x72\x22\x3e\x0a\x20\x20\x20\x20\x7b\x66\x6f\x6f\x74\x65\x72\x7d\x0a\x3c\x2f\x66\x6f\x6f\x74\x65\x72\x3e\x0a\x0a\x3c\x2f\x68\x74\x6d\x6c\x3e")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
|
||||||
|
f, err := FS.OpenFile(CTX, "/main.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_, err = f.Write(FileMainHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
35
api/http/static/b0xfile__navigation.html.go
Normal file
35
api/http/static/b0xfile__navigation.html.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Code generaTed by fileb0x at "2018-05-26 13:58:30.613412852 +0500 +05 m=+0.014560424" from config file "fileb0x.yml" DO NOT EDIT.
|
||||||
|
// modified(2018-05-26 13:57:14.88231462 +0500 +05)
|
||||||
|
// original path: assets/navigation.html
|
||||||
|
|
||||||
|
package static
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileNavigationHTML is "/navigation.html"
|
||||||
|
var FileNavigationHTML = []byte("\x3c\x6e\x61\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x20\x69\x73\x2d\x64\x61\x72\x6b\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x62\x72\x61\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x22\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x70\x61\x73\x74\x65\x73\x2f\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x50\x61\x73\x74\x65\x73\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x69\x64\x3d\x22\x6e\x61\x76\x62\x61\x72\x49\x74\x65\x6d\x73\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x6d\x65\x6e\x75\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x73\x74\x61\x72\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x65\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x6e\x61\x76\x3e")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
|
||||||
|
f, err := FS.OpenFile(CTX, "/navigation.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_, err = f.Write(FileNavigationHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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
|
@ -0,0 +1,35 @@
|
||||||
|
// Code generaTed by fileb0x at "2018-05-26 13:24:21.508025661 +0500 +05 m=+0.014883772" from config file "fileb0x.yml" DO NOT EDIT.
|
||||||
|
// modified(2018-05-26 13:24:18.605696269 +0500 +05)
|
||||||
|
// original path: assets/paste.html
|
||||||
|
|
||||||
|
package static
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilePasteHTML is "/paste.html"
|
||||||
|
var FilePasteHTML = []byte("\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6f\x6e\x74\x65\x6e\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x61\x62\x6c\x65\x20\x63\x6c\x61\x73\x73\x3d\x22\x74\x61\x62\x6c\x65\x20\x69\x73\x2d\x62\x6f\x72\x64\x65\x72\x65\x64\x20\x69\x73\x2d\x73\x74\x72\x69\x70\x65\x64\x20\x69\x73\x2d\x6e\x61\x72\x72\x6f\x77\x20\x69\x73\x2d\x68\x6f\x76\x65\x72\x61\x62\x6c\x65\x20\x69\x73\x2d\x66\x75\x6c\x6c\x77\x69\x64\x74\x68\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x23\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x54\x69\x74\x6c\x65\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x4c\x61\x6e\x67\x75\x61\x67\x65\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x50\x61\x73\x74\x65\x64\x20\x6f\x6e\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x57\x69\x6c\x6c\x20\x65\x78\x70\x69\x72\x65\x20\x6f\x6e\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x68\x3e\x50\x61\x73\x74\x65\x20\x74\x79\x70\x65\x3c\x2f\x74\x68\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x62\x6f\x64\x79\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x49\x44\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x54\x69\x74\x6c\x65\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x4c\x61\x6e\x67\x75\x61\x67\x65\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x44\x61\x74\x65\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x45\x78\x70\x69\x72\x61\x74\x69\x6f\x6e\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x3e\x7b\x70\x61\x73\x74\x65\x54\x79\x70\x65\x7d\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x74\x64\x20\x63\x6f\x6c\x73\x70\x61\x6e\x3d\x22\x36\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x62\x75\x74\x74\x6f\x6e\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x70\x61\x73\x74\x65\x2f\x7b\x70\x61\x73\x74\x65\x49\x44\x7d\x2f\x7b\x70\x61\x73\x74\x65\x54\x73\x7d\x72\x61\x77\x22\x3e\x56\x69\x65\x77\x20\x72\x61\x77\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x64\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x72\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x62\x6f\x64\x79\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x74\x61\x62\x6c\x65\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e\x0a\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x70\x61\x73\x74\x65\x2d\x64\x61\x74\x61\x22\x3e\x0a\x20\x20\x20\x20\x7b\x70\x61\x73\x74\x65\x64\x61\x74\x61\x7d\x0a\x3c\x2f\x64\x69\x76\x3e")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
|
||||||
|
f, err := FS.OpenFile(CTX, "/paste.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_, err = f.Write(FilePasteHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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-26 13:47:21.417964779 +0500 +05 m=+0.019030606" from config file "fileb0x.yml" DO NOT EDIT.
|
||||||
|
// modified(2018-05-26 13:41:01.75233841 +0500 +05)
|
||||||
|
// original path: assets/pastelist_list.html
|
||||||
|
|
||||||
|
package static
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilePastelistListHTML is "/pastelist_list.html"
|
||||||
|
var FilePastelistListHTML = []byte("\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x73\x74\x65\x73\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
|
||||||
|
f, err := FS.OpenFile(CTX, "/pastelist_list.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_, err = f.Write(FilePastelistListHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
package indexpage
|
package json
|
||||||
|
|
||||||
import (
|
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
|
// New initializes basic JSON API.
|
||||||
// endpoints.
|
|
||||||
func New(cc *context.Context) {
|
func New(cc *context.Context) {
|
||||||
ctx = cc
|
c = cc
|
||||||
|
c.Logger.Info().Msg("Initializing JSON API...")
|
||||||
ctx.Echo.GET("/", indexGet)
|
|
||||||
}
|
}
|
|
@ -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>
|
|
|
@ -5,7 +5,7 @@
|
||||||
<strong>{version}</strong> by
|
<strong>{version}</strong> by
|
||||||
<a href="https://pztrn.name">Stanislav N. aka pztrn</a>. The source code is licensed
|
<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="http://opensource.org/licenses/mit-license.php">MIT</a>. Get
|
||||||
<a href="https://code.pztrn.name/apps/fastpastebin">source or binary releases here</a>!
|
<a href="https://github.com/pztrn/fastpastebin">source or binary releases here</a>!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -38,13 +38,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<label class="checkbox">
|
||||||
<input type="checkbox" name="paste-private" id="paste-private"> Private paste with unique URL?
|
<input type="checkbox" name="paste-private" id="paste-private"> Private paste with unique URL?
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>OR</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>
|
<label for="paste-password">Password for paste:</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" placeholder="Enter password." name="paste-password" id="paste-password">
|
<input class="input" type="text" placeholder="Enter password." name="paste-password" id="paste-password">
|
||||||
|
@ -78,4 +79,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
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
|
@ -5,8 +5,8 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Fast Paste Bin</title>
|
<title>Fast Paste Bin</title>
|
||||||
<link rel="stylesheet" href="/static/css/bulma-0.9.4.min.css">
|
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
|
||||||
<link rel="stylesheet" href="/static/css/bulma-tooltip-1.2.min.css">
|
<link rel="stylesheet" href="/static/css/bulma-tooltip-1.0.4.min.css">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -17,4 +17,4 @@
|
||||||
{footer}
|
{footer}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</html>
|
</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
|
package captcha
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/dchest/captcha"
|
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
"github.com/rs/zerolog"
|
// local
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
"github.com/pztrn/fastpastebin/context"
|
||||||
|
|
||||||
|
// other
|
||||||
|
"github.com/dchest/captcha"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx *context.Context
|
c *context.Context
|
||||||
log zerolog.Logger
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// New initializes captcha package and adds necessary HTTP and API
|
// New initializes captcha package and adds neccessary HTTP and API
|
||||||
// endpoints.
|
// endpoints.
|
||||||
func New(cc *context.Context) {
|
func New(cc *context.Context) {
|
||||||
ctx = cc
|
c = cc
|
||||||
log = ctx.Logger.With().Str("type", "internal").Str("package", "captcha").Logger()
|
|
||||||
|
|
||||||
// New paste.
|
// 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.
|
// NewCaptcha creates new captcha string.
|
||||||
func NewCaptcha() string {
|
func NewCaptcha() string {
|
||||||
s := captcha.New()
|
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
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,54 +25,53 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
// stdlib
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"go.dev.pztrn.name/fastpastebin/domains/dbnotavailable"
|
// local
|
||||||
"go.dev.pztrn.name/fastpastebin/domains/indexpage"
|
"github.com/pztrn/fastpastebin/api"
|
||||||
"go.dev.pztrn.name/fastpastebin/domains/pastes"
|
"github.com/pztrn/fastpastebin/captcha"
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/captcha"
|
"github.com/pztrn/fastpastebin/context"
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
"github.com/pztrn/fastpastebin/database"
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/database"
|
"github.com/pztrn/fastpastebin/database/migrations"
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/templater"
|
"github.com/pztrn/fastpastebin/pastes"
|
||||||
|
"github.com/pztrn/fastpastebin/templater"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
appCtx := context.New()
|
c := context.New()
|
||||||
appCtx.Initialize()
|
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
|
// Here goes initial initialization for packages that want CLI flags
|
||||||
// to be added.
|
// to be added.
|
||||||
|
|
||||||
// Parse flags.
|
// Parse flags.
|
||||||
flag.Parse()
|
c.Flagger.Parse()
|
||||||
|
|
||||||
// Continue loading.
|
// Continue loading.
|
||||||
appCtx.LoadConfiguration()
|
c.LoadConfiguration()
|
||||||
appCtx.InitializePost()
|
database.New(c)
|
||||||
database.New(appCtx)
|
c.Database.Initialize()
|
||||||
appCtx.Database.Initialize()
|
migrations.New(c)
|
||||||
templater.Initialize(appCtx)
|
migrations.Migrate()
|
||||||
|
templater.Initialize(c)
|
||||||
|
api.New(c)
|
||||||
|
api.InitializeAPI()
|
||||||
|
|
||||||
captcha.New(appCtx)
|
captcha.New(c)
|
||||||
|
pastes.New(c)
|
||||||
dbnotavailable.New(appCtx)
|
|
||||||
indexpage.New(appCtx)
|
|
||||||
pastes.New(appCtx)
|
|
||||||
|
|
||||||
// CTRL+C handler.
|
// CTRL+C handler.
|
||||||
signalHandler := make(chan os.Signal, 1)
|
signalHandler := make(chan os.Signal, 1)
|
||||||
shutdownDone := make(chan bool, 1)
|
shutdownDone := make(chan bool, 1)
|
||||||
|
|
||||||
signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-signalHandler
|
<-signalHandler
|
||||||
appCtx.Shutdown()
|
c.Shutdown()
|
||||||
shutdownDone <- true
|
shutdownDone <- true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,8 @@
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// Database describes database configuration.
|
// ConfigDatabase describes database configuration.
|
||||||
type Database struct {
|
type ConfigDatabase struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
|
@ -24,10 +24,9 @@
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// HTTP describes HTTP server configuration.
|
// ConfigHTTP describes HTTP server configuration.
|
||||||
type HTTP struct {
|
type ConfigHTTP struct {
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
||||||
Port string `yaml:"port"`
|
Port string `yaml:"port"`
|
||||||
MaxBodySizeMegabytes string `yaml:"max_body_size_megabytes"`
|
AllowInsecure bool `yaml:"allow_insecure"`
|
||||||
AllowInsecure bool `yaml:"allow_insecure"`
|
|
||||||
}
|
}
|
|
@ -24,9 +24,9 @@
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// Logging describes logger configuration.
|
// ConfigLogging describes logger configuration.
|
||||||
type Logging struct {
|
type ConfigLogging struct {
|
||||||
|
LogToFile bool `yaml:"log_to_file"`
|
||||||
FileName string `yaml:"filename"`
|
FileName string `yaml:"filename"`
|
||||||
LogLevel string `yaml:"loglevel"`
|
LogLevel string `yaml:"loglevel"`
|
||||||
LogToFile bool `yaml:"log_to_file"`
|
|
||||||
}
|
}
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// Pastes describes pastes subsystem configuration.
|
// ConfigPastes describes pastes subsystem configuration.
|
||||||
type Pastes struct {
|
type ConfigPastes struct {
|
||||||
Pagination int `yaml:"pagination"`
|
Pagination int `yaml:"pagination"`
|
||||||
}
|
}
|
|
@ -24,10 +24,10 @@
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// Struct describes whole configuration.
|
// ConfigStruct describes whole configuration.
|
||||||
type Struct struct {
|
type ConfigStruct struct {
|
||||||
Database Database `yaml:"database"`
|
Database ConfigDatabase `yaml:"database"`
|
||||||
Logging Logging `yaml:"logging"`
|
Logging ConfigLogging `yaml:"logging"`
|
||||||
HTTP HTTP `yaml:"http"`
|
HTTP ConfigHTTP `yaml:"http"`
|
||||||
Pastes Pastes `yaml:"pastes"`
|
Pastes ConfigPastes `yaml:"pastes"`
|
||||||
}
|
}
|
|
@ -25,14 +25,19 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
// stdlib
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/config"
|
||||||
|
"github.com/pztrn/fastpastebin/database/interface"
|
||||||
|
|
||||||
|
// other
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
|
"github.com/pztrn/flagger"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/config"
|
|
||||||
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,24 +46,26 @@ import (
|
||||||
// contains everything every part of application need, like configuration
|
// contains everything every part of application need, like configuration
|
||||||
// access, logger, etc.
|
// access, logger, etc.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
Config *config.Struct
|
Config *config.ConfigStruct
|
||||||
Database databaseinterface.Interface
|
Database databaseinterface.Interface
|
||||||
Echo *echo.Echo
|
Echo *echo.Echo
|
||||||
Logger zerolog.Logger
|
Flagger *flagger.Flagger
|
||||||
configPathFromCLI string
|
Logger zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize initializes context.
|
// Initialize initializes context.
|
||||||
func (c *Context) Initialize() {
|
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.
|
c.Flagger.AddFlag(&flagger.Flag{
|
||||||
func (c *Context) InitializePost() {
|
Name: "config",
|
||||||
c.initializeLoggerPost()
|
Description: "Configuration file path. Can be overridded with FASTPASTEBIN_CONFIG environment variable (this is what used in tests).",
|
||||||
c.initializeHTTPServer()
|
Type: "string",
|
||||||
|
DefaultValue: "NO_CONFIG",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfiguration loads configuration and executes right after Flagger
|
// LoadConfiguration loads configuration and executes right after Flagger
|
||||||
|
@ -67,45 +74,63 @@ func (c *Context) InitializePost() {
|
||||||
func (c *Context) LoadConfiguration() {
|
func (c *Context) LoadConfiguration() {
|
||||||
c.Logger.Info().Msg("Loading configuration...")
|
c.Logger.Info().Msg("Loading configuration...")
|
||||||
|
|
||||||
configPath := c.configPathFromCLI
|
var configPath = ""
|
||||||
|
|
||||||
// We're accepting configuration path from "-config" CLI parameter
|
// We're accepting configuration path from "-config" CLI parameter
|
||||||
// and FASTPASTEBIN_CONFIG environment variable. Later have higher
|
// and FASTPASTEBIN_CONFIG environment variable. Later have higher
|
||||||
// weight and can override "-config" value.
|
// weight and can override "-config" value.
|
||||||
|
configPathFromCLI, err := c.Flagger.GetStringValue("config")
|
||||||
configPathFromEnv, configPathFromEnvFound := os.LookupEnv("FASTPASTEBIN_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.")
|
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.
|
// Normalize file path.
|
||||||
normalizedConfigPath, err1 := filepath.Abs(configPath)
|
normalizedConfigPath, err1 := filepath.Abs(configPath)
|
||||||
if err1 != nil {
|
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.ConfigStruct{}
|
||||||
c.Config = &config.Struct{}
|
|
||||||
|
|
||||||
// Read configuration file.
|
// Read configuration file.
|
||||||
fileData, err2 := os.ReadFile(normalizedConfigPath)
|
fileData, err2 := ioutil.ReadFile(normalizedConfigPath)
|
||||||
if err2 != nil {
|
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.
|
// Parse it into structure.
|
||||||
err3 := yaml.Unmarshal(fileData, c.Config)
|
err3 := yaml.Unmarshal(fileData, c.Config)
|
||||||
if err3 != nil {
|
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!
|
// Yay! See what it gets!
|
||||||
c.Logger.Debug().Msgf("Parsed configuration: %+v", c.Config)
|
c.Logger.Debug().Msgf("Parsed configuration: %+v", c.Config)
|
||||||
|
|
||||||
|
// Set log level.
|
||||||
|
c.Logger.Info().Msgf("Setting logger level: %s", c.Config.Logging.LogLevel)
|
||||||
|
switch c.Config.Logging.LogLevel {
|
||||||
|
case "DEBUG":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
case "INFO":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
case "WARN":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||||
|
case "ERROR":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
case "FATAL":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||||
|
case "PANIC":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.PanicLevel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterDatabaseInterface registers database interface for later use.
|
// RegisterDatabaseInterface registers database interface for later use.
|
|
@ -26,11 +26,10 @@ package context
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Version .
|
// Version .
|
||||||
Version = "0.4.1"
|
Version = "0.2.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New creates new context.
|
// New creates new context.
|
||||||
func New() *Context {
|
func New() *Context {
|
||||||
//nolint:exhaustruct
|
|
||||||
return &Context{}
|
return &Context{}
|
||||||
}
|
}
|
90
database/database.go
Normal file
90
database/database.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// 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
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/database/dialects/flatfiles"
|
||||||
|
"github.com/pztrn/fastpastebin/database/dialects/interface"
|
||||||
|
"github.com/pztrn/fastpastebin/database/dialects/mysql"
|
||||||
|
"github.com/pztrn/fastpastebin/pastes/model"
|
||||||
|
|
||||||
|
// other
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database represents control structure for database connection.
|
||||||
|
type Database struct {
|
||||||
|
db dialectinterface.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetDatabaseConnection() *sql.DB {
|
||||||
|
if db.db != nil {
|
||||||
|
return db.db.GetDatabaseConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
|
||||||
|
return db.db.GetPaste(pasteID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
|
||||||
|
return db.db.GetPagedPastes(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetPastesPages() int {
|
||||||
|
return db.db.GetPastesPages()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes connection to database.
|
||||||
|
func (db *Database) Initialize() {
|
||||||
|
c.Logger.Info().Msg("Initializing database connection...")
|
||||||
|
|
||||||
|
if c.Config.Database.Type == "mysql" {
|
||||||
|
mysql.New(c)
|
||||||
|
} else if c.Config.Database.Type == "flatfiles" {
|
||||||
|
flatfiles.New(c)
|
||||||
|
} else {
|
||||||
|
c.Logger.Fatal().Msgf("Unknown database type: %s", c.Config.Database.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) RegisterDialect(di dialectinterface.Interface) {
|
||||||
|
db.db = di
|
||||||
|
db.db.Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) SavePaste(p *pastesmodel.Paste) (int64, error) {
|
||||||
|
return db.db.SavePaste(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) Shutdown() {
|
||||||
|
db.db.Shutdown()
|
||||||
|
}
|
|
@ -22,19 +22,21 @@
|
||||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
package dbnotavailable
|
package flatfiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/context"
|
||||||
|
"github.com/pztrn/fastpastebin/database/dialects/interface"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ctx *context.Context
|
var (
|
||||||
|
c *context.Context
|
||||||
|
f *FlatFiles
|
||||||
|
)
|
||||||
|
|
||||||
// New initializes pastes package and adds necessary HTTP and API
|
|
||||||
// endpoints.
|
|
||||||
func New(cc *context.Context) {
|
func New(cc *context.Context) {
|
||||||
ctx = cc
|
c = cc
|
||||||
|
f = &FlatFiles{}
|
||||||
ctx.Echo.GET("/database_not_available", dbNotAvailableGet)
|
c.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
|
||||||
ctx.Echo.GET("/database_not_available/raw", dbNotAvailableRawGet)
|
|
||||||
}
|
}
|
246
database/dialects/flatfiles/flatfiles.go
Normal file
246
database/dialects/flatfiles/flatfiles.go
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
// 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 (
|
||||||
|
// stdlib
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/pastes/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FlatFiles struct {
|
||||||
|
pastesIndex []*Index
|
||||||
|
path string
|
||||||
|
writeMutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ff *FlatFiles) GetDatabaseConnection() *sql.DB {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ff *FlatFiles) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
|
||||||
|
ff.writeMutex.Lock()
|
||||||
|
pastePath := filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json")
|
||||||
|
c.Logger.Debug().Msgf("Trying to load paste data from '%s'...", pastePath)
|
||||||
|
pasteInBytes, err := ioutil.ReadFile(pastePath)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Debug().Msgf("Failed to read paste from storage: %s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.Logger.Debug().Msgf("Loaded %d bytes: %s", len(pasteInBytes), string(pasteInBytes))
|
||||||
|
ff.writeMutex.Unlock()
|
||||||
|
|
||||||
|
paste := &pastesmodel.Paste{}
|
||||||
|
err = json.Unmarshal(pasteInBytes, paste)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error().Msgf("Failed to parse paste: %s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return paste, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ff *FlatFiles) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
|
||||||
|
// Pagination.
|
||||||
|
var startPagination = 0
|
||||||
|
if page > 1 {
|
||||||
|
startPagination = (page - 1) * c.Config.Pastes.Pagination
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Logger.Debug().Msgf("Pastes index: %+v", ff.pastesIndex)
|
||||||
|
|
||||||
|
// Iteration one - get only public pastes.
|
||||||
|
var publicPastes []*Index
|
||||||
|
for _, paste := range ff.pastesIndex {
|
||||||
|
if !paste.Private {
|
||||||
|
publicPastes = append(publicPastes, paste)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Logger.Debug().Msgf("%+v", publicPastes)
|
||||||
|
|
||||||
|
// Iteration two - get paginated pastes.
|
||||||
|
var pastesData []pastesmodel.Paste
|
||||||
|
for idx, paste := range publicPastes {
|
||||||
|
if len(pastesData) == c.Config.Pastes.Pagination {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx < startPagination {
|
||||||
|
c.Logger.Debug().Msgf("Paste with index %d isn't in pagination query: too low index", idx)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx-1 >= startPagination && page > 1 && idx > startPagination+((page-1)*c.Config.Pastes.Pagination)) || (idx-1 >= startPagination && page == 1 && idx > startPagination+(page*c.Config.Pastes.Pagination)) {
|
||||||
|
c.Logger.Debug().Msgf("Paste with index %d isn't in pagination query: too high index", idx)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c.Logger.Debug().Msgf("Getting paste data (ID: %d, index: %d)", paste.ID, idx)
|
||||||
|
|
||||||
|
// Get paste data.
|
||||||
|
pasteData := &pastesmodel.Paste{}
|
||||||
|
pasteRawData, err := ioutil.ReadFile(filepath.Join(ff.path, "pastes", strconv.Itoa(paste.ID)+".json"))
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error().Msgf("Failed to read paste data: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(pasteRawData, pasteData)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error().Msgf("Failed to parse paste data: %s", err.Error())
|
||||||
|
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) / c.Config.Pastes.Pagination
|
||||||
|
// Check if we have any remainder. Add 1 to pages count if so.
|
||||||
|
if len(publicPastes)%c.Config.Pastes.Pagination > 0 {
|
||||||
|
pages++
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ff *FlatFiles) Initialize() {
|
||||||
|
c.Logger.Info().Msg("Initializing flatfiles storage...")
|
||||||
|
|
||||||
|
path := c.Config.Database.Path
|
||||||
|
// Get proper paste file path.
|
||||||
|
if strings.Contains(c.Config.Database.Path, "~") {
|
||||||
|
curUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
c.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
|
||||||
|
c.Logger.Debug().Msgf("Storage path is now: %s", ff.path)
|
||||||
|
|
||||||
|
// Create directory if neccessary.
|
||||||
|
if _, err := os.Stat(ff.path); err != nil {
|
||||||
|
c.Logger.Debug().Msgf("Directory '%s' does not exist, creating...", ff.path)
|
||||||
|
os.MkdirAll(ff.path, os.ModePerm)
|
||||||
|
} else {
|
||||||
|
c.Logger.Debug().Msgf("Directory '%s' already exists", ff.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directory for pastes.
|
||||||
|
if _, err := os.Stat(filepath.Join(ff.path, "pastes")); err != nil {
|
||||||
|
c.Logger.Debug().Msgf("Directory '%s' does not exist, creating...", filepath.Join(ff.path, "pastes"))
|
||||||
|
os.MkdirAll(filepath.Join(ff.path, "pastes"), os.ModePerm)
|
||||||
|
} else {
|
||||||
|
c.Logger.Debug().Msgf("Directory '%s' already exists", filepath.Join(ff.path, "pastes"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load pastes index.
|
||||||
|
ff.pastesIndex = []*Index{}
|
||||||
|
if _, err := os.Stat(filepath.Join(ff.path, "pastes", "index.json")); err != nil {
|
||||||
|
c.Logger.Warn().Msg("Pastes index file does not exist, will create new one")
|
||||||
|
} else {
|
||||||
|
indexData, err := ioutil.ReadFile(filepath.Join(ff.path, "pastes", "index.json"))
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Fatal().Msg("Failed to read contents of index file!")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(indexData, &ff.pastesIndex)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error().Msgf("Failed to parse index file contents from JSON into internal structure. Will create new index file. All of your previous pastes will became unavailable. Error was: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Logger.Debug().Msgf("Parsed pastes index: %+v", ff.pastesIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ff *FlatFiles) SavePaste(p *pastesmodel.Paste) (int64, error) {
|
||||||
|
ff.writeMutex.Lock()
|
||||||
|
// Write paste data on disk.
|
||||||
|
filesOnDisk, _ := ioutil.ReadDir(filepath.Join(ff.path, "pastes"))
|
||||||
|
pasteID := len(filesOnDisk) + 1
|
||||||
|
c.Logger.Debug().Msgf("Writing paste to disk, ID will be " + strconv.Itoa(pasteID))
|
||||||
|
p.ID = pasteID
|
||||||
|
data, err := json.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
ff.writeMutex.Unlock()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json"), data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
ff.writeMutex.Unlock()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// Add it to cache.
|
||||||
|
indexData := &Index{}
|
||||||
|
indexData.ID = pasteID
|
||||||
|
indexData.Private = p.Private
|
||||||
|
ff.pastesIndex = append(ff.pastesIndex, indexData)
|
||||||
|
ff.writeMutex.Unlock()
|
||||||
|
return int64(pasteID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ff *FlatFiles) Shutdown() {
|
||||||
|
c.Logger.Info().Msg("Saving indexes...")
|
||||||
|
indexData, err := json.Marshal(ff.pastesIndex)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error().Msgf("Failed to encode index data into JSON: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(filepath.Join(ff.path, "pastes", "index.json"), indexData, 0644)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error().Msgf("Failed to write index data to file. Pretty sure that you've lost your pastes.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,41 +25,39 @@
|
||||||
package flatfiles
|
package flatfiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/pastes/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct{}
|
type Handler struct{}
|
||||||
|
|
||||||
func (dbh Handler) DeletePaste(pasteID int) error {
|
|
||||||
return flf.DeletePaste(pasteID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
||||||
return flf.GetDatabaseConnection()
|
return f.GetDatabaseConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
|
func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
|
||||||
return flf.GetPaste(pasteID)
|
return f.GetPaste(pasteID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
|
func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
|
||||||
return flf.GetPagedPastes(page)
|
return f.GetPagedPastes(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) GetPastesPages() int {
|
func (dbh Handler) GetPastesPages() int {
|
||||||
return flf.GetPastesPages()
|
return f.GetPastesPages()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) Initialize() {
|
func (dbh Handler) Initialize() {
|
||||||
flf.Initialize()
|
f.Initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
|
func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) {
|
||||||
return flf.SavePaste(p)
|
return f.SavePaste(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) Shutdown() {
|
func (dbh Handler) Shutdown() {
|
||||||
flf.Shutdown()
|
f.Shutdown()
|
||||||
}
|
}
|
|
@ -25,18 +25,19 @@
|
||||||
package dialectinterface
|
package dialectinterface
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/pastes/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
DeletePaste(int) error
|
|
||||||
GetDatabaseConnection() *sql.DB
|
GetDatabaseConnection() *sql.DB
|
||||||
GetPaste(pasteID int) (*structs.Paste, error)
|
GetPaste(pasteID int) (*pastesmodel.Paste, error)
|
||||||
GetPagedPastes(page int) ([]structs.Paste, error)
|
GetPagedPastes(page int) ([]pastesmodel.Paste, error)
|
||||||
GetPastesPages() int
|
GetPastesPages() int
|
||||||
Initialize()
|
Initialize()
|
||||||
SavePaste(p *structs.Paste) (int64, error)
|
SavePaste(p *pastesmodel.Paste) (int64, error)
|
||||||
Shutdown()
|
Shutdown()
|
||||||
}
|
}
|
|
@ -25,19 +25,18 @@
|
||||||
package mysql
|
package mysql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
// local
|
||||||
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
|
"github.com/pztrn/fastpastebin/context"
|
||||||
|
"github.com/pztrn/fastpastebin/database/dialects/interface"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx *context.Context
|
c *context.Context
|
||||||
dbAdapter *Database
|
d *Database
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(cc *context.Context) {
|
func New(cc *context.Context) {
|
||||||
ctx = cc
|
c = cc
|
||||||
//nolint:exhaustruct
|
d = &Database{}
|
||||||
dbAdapter = &Database{}
|
c.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
|
||||||
|
|
||||||
ctx.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
|
|
||||||
}
|
}
|
|
@ -25,41 +25,39 @@
|
||||||
package mysql
|
package mysql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/pastes/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct{}
|
type Handler struct{}
|
||||||
|
|
||||||
func (dbh Handler) DeletePaste(pasteID int) error {
|
|
||||||
return dbAdapter.DeletePaste(pasteID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
||||||
return dbAdapter.GetDatabaseConnection()
|
return d.GetDatabaseConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
|
func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
|
||||||
return dbAdapter.GetPaste(pasteID)
|
return d.GetPaste(pasteID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
|
func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
|
||||||
return dbAdapter.GetPagedPastes(page)
|
return d.GetPagedPastes(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) GetPastesPages() int {
|
func (dbh Handler) GetPastesPages() int {
|
||||||
return dbAdapter.GetPastesPages()
|
return d.GetPastesPages()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) Initialize() {
|
func (dbh Handler) Initialize() {
|
||||||
dbAdapter.Initialize()
|
d.Initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
|
func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) {
|
||||||
return dbAdapter.SavePaste(p)
|
return d.SavePaste(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) Shutdown() {
|
func (dbh Handler) Shutdown() {
|
||||||
dbAdapter.Shutdown()
|
d.Shutdown()
|
||||||
}
|
}
|
|
@ -25,14 +25,16 @@
|
||||||
package mysql
|
package mysql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
// MySQL driver.
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/pastes/model"
|
||||||
|
|
||||||
|
// other
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
"github.com/jmoiron/sqlx"
|
"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.
|
// Database is a MySQL/MariaDB connection controlling structure.
|
||||||
|
@ -40,75 +42,33 @@ type Database struct {
|
||||||
db *sqlx.DB
|
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 {
|
func (db *Database) GetDatabaseConnection() *sql.DB {
|
||||||
db.check()
|
return db.db.DB
|
||||||
|
|
||||||
if db.db != nil {
|
|
||||||
return db.db.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPaste returns a single paste by ID.
|
// GetPaste returns a single paste by ID.
|
||||||
func (db *Database) GetPaste(pasteID int) (*structs.Paste, error) {
|
func (db *Database) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
|
||||||
db.check()
|
p := &pastesmodel.Paste{}
|
||||||
|
err := db.db.Get(p, db.db.Rebind("SELECT * FROM `pastes` WHERE id=?"), pasteID)
|
||||||
//nolint:exhaustruct
|
|
||||||
paste := &structs.Paste{}
|
|
||||||
|
|
||||||
err := db.db.Get(paste, db.db.Rebind("SELECT * FROM `pastes` WHERE id=?"), pasteID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return paste, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) {
|
func (db *Database) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
|
||||||
db.check()
|
var pastesRaw []pastesmodel.Paste
|
||||||
|
var pastes []pastesmodel.Paste
|
||||||
var (
|
|
||||||
pastesRaw []structs.Paste
|
|
||||||
pastes []structs.Paste
|
|
||||||
)
|
|
||||||
|
|
||||||
// Pagination.
|
// Pagination.
|
||||||
startPagination := 0
|
var startPagination = 0
|
||||||
if page > 1 {
|
if page > 1 {
|
||||||
startPagination = (page - 1) * ctx.Config.Pastes.Pagination
|
startPagination = (page - 1) * c.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)
|
err := db.db.Select(&pastesRaw, db.db.Rebind("SELECT * FROM `pastes` WHERE private != true ORDER BY id DESC LIMIT ? OFFSET ?"), c.Config.Pastes.Pagination, startPagination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,13 +82,8 @@ func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) GetPastesPages() int {
|
func (db *Database) GetPastesPages() int {
|
||||||
db.check()
|
var pastesRaw []pastesmodel.Paste
|
||||||
|
var pastes []pastesmodel.Paste
|
||||||
var (
|
|
||||||
pastesRaw []structs.Paste
|
|
||||||
pastes []structs.Paste
|
|
||||||
)
|
|
||||||
|
|
||||||
err := db.db.Get(&pastesRaw, "SELECT * FROM `pastes` WHERE private != true")
|
err := db.db.Get(&pastesRaw, "SELECT * FROM `pastes` WHERE private != true")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -142,9 +97,9 @@ func (db *Database) GetPastesPages() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate pages.
|
// Calculate pages.
|
||||||
pages := len(pastes) / ctx.Config.Pastes.Pagination
|
pages := len(pastes) / c.Config.Pastes.Pagination
|
||||||
// Check if we have any remainder. Add 1 to pages count if so.
|
// Check if we have any remainder. Add 1 to pages count if so.
|
||||||
if len(pastes)%ctx.Config.Pastes.Pagination > 0 {
|
if len(pastes)%c.Config.Pastes.Pagination > 0 {
|
||||||
pages++
|
pages++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,62 +108,49 @@ func (db *Database) GetPastesPages() int {
|
||||||
|
|
||||||
// Initialize initializes MySQL/MariaDB connection.
|
// Initialize initializes MySQL/MariaDB connection.
|
||||||
func (db *Database) Initialize() {
|
func (db *Database) Initialize() {
|
||||||
ctx.Logger.Info().Msg("Initializing database connection...")
|
c.Logger.Info().Msg("Initializing database connection...")
|
||||||
|
|
||||||
// There might be only user, without password. MySQL/MariaDB driver
|
// There might be only user, without password. MySQL/MariaDB driver
|
||||||
// in DSN wants "user" or "user:password", "user:" is invalid.
|
// in DSN wants "user" or "user:password", "user:" is invalid.
|
||||||
var userpass string
|
var userpass = ""
|
||||||
if ctx.Config.Database.Password == "" {
|
if c.Config.Database.Password == "" {
|
||||||
userpass = ctx.Config.Database.Username
|
userpass = c.Config.Database.Username
|
||||||
} else {
|
} else {
|
||||||
userpass = ctx.Config.Database.Username + ":" + ctx.Config.Database.Password
|
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, ctx.Config.Database.Address, ctx.Config.Database.Port, ctx.Config.Database.Database)
|
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)
|
||||||
ctx.Logger.Debug().Str("DSN", dbConnString).Msgf("Database connection string composed")
|
c.Logger.Debug().Msgf("Database connection string: %s", dbConnString)
|
||||||
|
|
||||||
dbConn, err := sqlx.Connect("mysql", dbConnString)
|
dbConn, err := sqlx.Connect("mysql", dbConnString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Error().Err(err).Msg("Failed to connect to database")
|
c.Logger.Panic().Msgf("Failed to connect to database: %s", err.Error())
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force UTC for current connection.
|
// Force UTC for current connection.
|
||||||
_ = dbConn.MustExec("SET @@session.time_zone='+00:00';")
|
_ = dbConn.MustExec("SET @@session.time_zone='+00:00';")
|
||||||
|
|
||||||
ctx.Logger.Info().Msg("Database connection established")
|
c.Logger.Info().Msg("Database connection established")
|
||||||
|
|
||||||
db.db = dbConn
|
db.db = dbConn
|
||||||
|
|
||||||
// Perform migrations.
|
|
||||||
migrations.New(ctx)
|
|
||||||
migrations.Migrate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) SavePaste(p *structs.Paste) (int64, error) {
|
func (db *Database) SavePaste(p *pastesmodel.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)
|
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 {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lastInsertID, err1 := result.LastInsertId()
|
ID, err1 := result.LastInsertId()
|
||||||
if err1 != nil {
|
if err1 != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastInsertID, nil
|
return ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) Shutdown() {
|
func (db *Database) Shutdown() {
|
||||||
if db.db != nil {
|
err := db.db.Close()
|
||||||
err := db.db.Close()
|
if err != nil {
|
||||||
if err != nil {
|
c.Logger.Error().Msgf("Failed to close database connection: %s", err.Error())
|
||||||
ctx.Logger.Error().Err(err).Msg("Failed to close database connection")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -25,20 +25,19 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
// local
|
||||||
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
|
"github.com/pztrn/fastpastebin/context"
|
||||||
|
"github.com/pztrn/fastpastebin/database/interface"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx *context.Context
|
c *context.Context
|
||||||
dbAdapter *Database
|
d *Database
|
||||||
)
|
)
|
||||||
|
|
||||||
// New initializes database structure.
|
// New initializes database structure.
|
||||||
func New(cc *context.Context) {
|
func New(cc *context.Context) {
|
||||||
ctx = cc
|
c = cc
|
||||||
//nolint:exhaustruct
|
d = &Database{}
|
||||||
dbAdapter = &Database{}
|
c.RegisterDatabaseInterface(databaseinterface.Interface(Handler{}))
|
||||||
|
|
||||||
ctx.RegisterDatabaseInterface(databaseinterface.Interface(Handler{}))
|
|
||||||
}
|
}
|
|
@ -25,49 +25,47 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
|
// local
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
"github.com/pztrn/fastpastebin/database/dialects/interface"
|
||||||
|
"github.com/pztrn/fastpastebin/pastes/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler is an interfaceable structure that proxifies calls from anyone
|
// Handler is an interfaceable structure that proxifies calls from anyone
|
||||||
// to Database structure.
|
// to Database structure.
|
||||||
type Handler struct{}
|
type Handler struct{}
|
||||||
|
|
||||||
func (dbh Handler) DeletePaste(pasteID int) error {
|
|
||||||
return dbAdapter.DeletePaste(pasteID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
func (dbh Handler) GetDatabaseConnection() *sql.DB {
|
||||||
return dbAdapter.GetDatabaseConnection()
|
return d.GetDatabaseConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
|
func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
|
||||||
return dbAdapter.GetPaste(pasteID)
|
return d.GetPaste(pasteID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
|
func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
|
||||||
return dbAdapter.GetPagedPastes(page)
|
return d.GetPagedPastes(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) GetPastesPages() int {
|
func (dbh Handler) GetPastesPages() int {
|
||||||
return dbAdapter.GetPastesPages()
|
return d.GetPastesPages()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize initializes connection to database.
|
// Initialize initializes connection to database.
|
||||||
func (dbh Handler) Initialize() {
|
func (dbh Handler) Initialize() {
|
||||||
dbAdapter.Initialize()
|
d.Initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) RegisterDialect(di dialectinterface.Interface) {
|
func (dbh Handler) RegisterDialect(di dialectinterface.Interface) {
|
||||||
dbAdapter.RegisterDialect(di)
|
d.RegisterDialect(di)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
|
func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) {
|
||||||
return dbAdapter.SavePaste(p)
|
return d.SavePaste(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbh Handler) Shutdown() {
|
func (dbh Handler) Shutdown() {
|
||||||
dbAdapter.Shutdown()
|
d.Shutdown()
|
||||||
}
|
}
|
|
@ -25,22 +25,23 @@
|
||||||
package databaseinterface
|
package databaseinterface
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
|
// local
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/structs"
|
"github.com/pztrn/fastpastebin/database/dialects/interface"
|
||||||
|
"github.com/pztrn/fastpastebin/pastes/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interface represents database interface which is available to all
|
// Interface represents database interface which is available to all
|
||||||
// parts of application and registers with context.Context.
|
// parts of application and registers with context.Context.
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
DeletePaste(int) error
|
|
||||||
GetDatabaseConnection() *sql.DB
|
GetDatabaseConnection() *sql.DB
|
||||||
GetPaste(pasteID int) (*structs.Paste, error)
|
GetPaste(pasteID int) (*pastesmodel.Paste, error)
|
||||||
GetPagedPastes(page int) ([]structs.Paste, error)
|
GetPagedPastes(page int) ([]pastesmodel.Paste, error)
|
||||||
GetPastesPages() int
|
GetPastesPages() int
|
||||||
Initialize()
|
Initialize()
|
||||||
RegisterDialect(dialectinterface.Interface)
|
RegisterDialect(dialectinterface.Interface)
|
||||||
SavePaste(p *structs.Paste) (int64, error)
|
SavePaste(p *pastesmodel.Paste) (int64, error)
|
||||||
Shutdown()
|
Shutdown()
|
||||||
}
|
}
|
|
@ -25,20 +25,13 @@
|
||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitialUp(tx *sql.Tx) error {
|
func InitialUp(tx *sql.Tx) error {
|
||||||
_, err := tx.Exec(`CREATE TABLE 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';")
|
||||||
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 {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,13 @@
|
||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PasteLangUp(tx *sql.Tx) error {
|
func PasteLangUp(tx *sql.Tx) error {
|
||||||
_, err := tx.Exec("ALTER TABLE `pastes` ADD `language` VARCHAR(191) NOT NULL DEFAULT 'text' COMMENT 'Paste language'")
|
_, err := tx.Exec("ALTER TABLE `pastes` ADD `language` VARCHAR(191) NOT NULL DEFAULT 'text' COMMENT 'Paste language'")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,6 @@ func PasteLangUp(tx *sql.Tx) error {
|
||||||
func PasteLangDown(tx *sql.Tx) error {
|
func PasteLangDown(tx *sql.Tx) error {
|
||||||
_, err := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `language`")
|
_, err := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `language`")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,13 @@
|
||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PrivatePastesUp(tx *sql.Tx) error {
|
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.'")
|
_, 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 {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,6 @@ func PrivatePastesUp(tx *sql.Tx) error {
|
||||||
func PrivatePastesDown(tx *sql.Tx) error {
|
func PrivatePastesDown(tx *sql.Tx) error {
|
||||||
_, err := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `private`")
|
_, err := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `private`")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,35 +25,32 @@
|
||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PasswordedPastesUp(txn *sql.Tx) error {
|
func PasswordedPastesUp(tx *sql.Tx) error {
|
||||||
_, err := txn.Exec("ALTER TABLE `pastes` ADD `password` varchar(64) NOT NULL DEFAULT '' COMMENT 'Password for paste (scrypted and sha256ed).'")
|
_, err := tx.Exec("ALTER TABLE `pastes` ADD `password` varchar(64) NOT NULL DEFAULT '' COMMENT 'Password for paste (scrypted and sha256ed).'")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return err
|
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 {
|
if err1 != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return err1
|
return err1
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PasswordedPastesDown(txn *sql.Tx) error {
|
func PasswordedPastesDown(tx *sql.Tx) error {
|
||||||
_, err := txn.Exec("ALTER TABLE `pastes` DROP COLUMN `password`")
|
_, err := tx.Exec("ALTER TABLE `pastes` DROP COLUMN `password`")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return err
|
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 {
|
if err1 != nil {
|
||||||
//nolint:wrapcheck
|
|
||||||
return err1
|
return err1
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,35 +25,41 @@
|
||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/context"
|
||||||
|
|
||||||
|
// other
|
||||||
|
//"github.com/jmoiron/sqlx"
|
||||||
"github.com/pressly/goose"
|
"github.com/pressly/goose"
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ctx *context.Context
|
var (
|
||||||
|
c *context.Context
|
||||||
|
)
|
||||||
|
|
||||||
// New initializes migrations.
|
// New initializes migrations.
|
||||||
func New(cc *context.Context) {
|
func New(cc *context.Context) {
|
||||||
ctx = cc
|
c = cc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate launching migrations.
|
// Migrate launching migrations.
|
||||||
func Migrate() {
|
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("1_initial.go", InitialUp, nil)
|
||||||
goose.AddNamedMigration("2_paste_lang.go", PasteLangUp, PasteLangDown)
|
goose.AddNamedMigration("2_paste_lang.go", PasteLangUp, PasteLangDown)
|
||||||
goose.AddNamedMigration("3_private_pastes.go", PrivatePastesUp, PrivatePastesDown)
|
goose.AddNamedMigration("3_private_pastes.go", PrivatePastesUp, PrivatePastesDown)
|
||||||
goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown)
|
goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown)
|
||||||
// Add new migrations BEFORE this message.
|
// Add new migrations BEFORE this message.
|
||||||
|
|
||||||
dbConn := ctx.Database.GetDatabaseConnection()
|
dbConn := c.Database.GetDatabaseConnection()
|
||||||
if dbConn != nil {
|
if dbConn != nil {
|
||||||
err := goose.Up(dbConn, ".")
|
err := goose.Up(dbConn, ".")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error())
|
c.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping")
|
c.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping")
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -2,9 +2,9 @@
|
||||||
# Only MySQL database and flatfiles are supported for now.
|
# Only MySQL database and flatfiles are supported for now.
|
||||||
database:
|
database:
|
||||||
# Database type. The only supported ATM is "mysql" and "flatfiles".
|
# Database type. The only supported ATM is "mysql" and "flatfiles".
|
||||||
type: "flatfiles"
|
type: "mysql"
|
||||||
# Path for data stored with "flatfiles" database adapter.
|
# Path for data stored with "flatfiles" database adapter.
|
||||||
# Will be completely ignored for MySQL/MariaDB.
|
# Will be comletely ignored for MySQL/MariaDB.
|
||||||
path: "./data"
|
path: "./data"
|
||||||
# Next parameters are strictly for MySQL/MariaDB connections and
|
# Next parameters are strictly for MySQL/MariaDB connections and
|
||||||
# will be ignored by "flatfiles" adapter.
|
# will be ignored by "flatfiles" adapter.
|
||||||
|
@ -25,14 +25,12 @@ logging:
|
||||||
|
|
||||||
# HTTP server configuration.
|
# HTTP server configuration.
|
||||||
http:
|
http:
|
||||||
address: "127.0.0.1"
|
address: "192.168.0.14"
|
||||||
port: "25544"
|
port: "25544"
|
||||||
# By default we're allowing only HTTPS requests. Setting this to true
|
# By default we're allowing only HTTPS requests. Setting this to true
|
||||||
# will allow HTTP requests. Useful for developing or if you're
|
# will allow HTTP requests. Useful for developing or if you're
|
||||||
# running Fast Pastebin behind reverse proxy that does SSL termination.
|
# running Fast Pastebin behind reverse proxy that does SSL termination.
|
||||||
allow_insecure: true
|
allow_insecure: true
|
||||||
# Maximum body size in megabytes. 1 should be enough for most use cases.
|
|
||||||
max_body_size_megabytes: 1
|
|
||||||
|
|
||||||
# Pastes configuration.
|
# Pastes configuration.
|
||||||
pastes:
|
pastes:
|
||||||
|
|
111
fileb0x.yml
Normal file
111
fileb0x.yml
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
# all folders and files are relative to the path
|
||||||
|
# where fileb0x was run at!
|
||||||
|
|
||||||
|
# default: main
|
||||||
|
pkg: static
|
||||||
|
|
||||||
|
# destination
|
||||||
|
dest: "./api/http/static/"
|
||||||
|
|
||||||
|
# gofmt
|
||||||
|
# type: bool
|
||||||
|
# default: false
|
||||||
|
fmt: false
|
||||||
|
|
||||||
|
# compress files
|
||||||
|
# at the moment, only supports gzip
|
||||||
|
#
|
||||||
|
# type: object
|
||||||
|
compression:
|
||||||
|
# activates the compression
|
||||||
|
#
|
||||||
|
# type: bool
|
||||||
|
# default: false
|
||||||
|
compress: false
|
||||||
|
|
||||||
|
# valid values are:
|
||||||
|
# -> "NoCompression"
|
||||||
|
# -> "BestSpeed"
|
||||||
|
# -> "BestCompression"
|
||||||
|
# -> "DefaultCompression" or ""
|
||||||
|
#
|
||||||
|
# type: string
|
||||||
|
# default: "DefaultCompression" # when: Compress == true && Method == ""
|
||||||
|
method: ""
|
||||||
|
|
||||||
|
# true = do it yourself (the file is written as gzip compressed file into the memory file system)
|
||||||
|
# false = decompress files at run time (while writing file into memory file system)
|
||||||
|
#
|
||||||
|
# type: bool
|
||||||
|
# default: false
|
||||||
|
keep: false
|
||||||
|
|
||||||
|
# ---------------
|
||||||
|
# -- DANGEROUS --
|
||||||
|
# ---------------
|
||||||
|
#
|
||||||
|
# cleans the destination folder (only b0xfiles)
|
||||||
|
# you should use this when using the spread function
|
||||||
|
# type: bool
|
||||||
|
# default: false
|
||||||
|
clean: true
|
||||||
|
|
||||||
|
# default: ab0x.go
|
||||||
|
output: "ab0x.go"
|
||||||
|
|
||||||
|
# [unexporTed] builds non-exporTed functions, variables and types...
|
||||||
|
# type: bool
|
||||||
|
# default: false
|
||||||
|
unexporTed: false
|
||||||
|
|
||||||
|
# [spread] means it will make a file to hold all fileb0x data
|
||||||
|
# and each file into a separaTed .go file
|
||||||
|
#
|
||||||
|
# example:
|
||||||
|
# theres 2 files in the folder assets, they're: hello.json and world.txt
|
||||||
|
# when spread is activaTed, fileb0x will make a file:
|
||||||
|
# b0x.go or [output]'s data, assets_hello.json.go and assets_world.txt.go
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# type: bool
|
||||||
|
# default: false
|
||||||
|
spread: true
|
||||||
|
|
||||||
|
# [lcf] log changed files when spread is active
|
||||||
|
lcf: true
|
||||||
|
|
||||||
|
# type: array of objects
|
||||||
|
custom:
|
||||||
|
|
||||||
|
# type: array of strings
|
||||||
|
- files:
|
||||||
|
- "assets/css/bulma-0.7.0.min.css"
|
||||||
|
- "assets/css/bulma-tooltip-1.0.4.min.css"
|
||||||
|
- "assets/css/bulma.css.map"
|
||||||
|
- "assets/css/style.css"
|
||||||
|
- "assets/js/fontawesome-5.0.7.js"
|
||||||
|
|
||||||
|
# base is the path that will be removed from all files' path
|
||||||
|
# type: string
|
||||||
|
base: "assets"
|
||||||
|
|
||||||
|
# prefix is the path that will be added to all files' path
|
||||||
|
# type: string
|
||||||
|
prefix: "static/"
|
||||||
|
# end: files
|
||||||
|
- files:
|
||||||
|
- "assets/error.html"
|
||||||
|
- "assets/footer.html"
|
||||||
|
- "assets/index.html"
|
||||||
|
- "assets/main.html"
|
||||||
|
- "assets/navigation.html"
|
||||||
|
- "assets/pagination_ellipsis.html"
|
||||||
|
- "assets/pagination_link_current.html"
|
||||||
|
- "assets/pagination_link.html"
|
||||||
|
- "assets/pagination.html"
|
||||||
|
- "assets/passworded_paste_verify.html"
|
||||||
|
- "assets/paste.html"
|
||||||
|
- "assets/pastelist_list.html"
|
||||||
|
- "assets/pastelist_paste.html"
|
||||||
|
base: "assets"
|
||||||
|
prefix: ""
|
23
go.mod
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 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,61 +1,56 @@
|
||||||
package pagination
|
package pagination
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// stdlib
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"go.dev.pztrn.name/fastpastebin/assets"
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/api/http/static"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateHTML creates pagination HTML based on passed parameters.
|
// CreateHTML creates pagination HTML based on passed parameters.
|
||||||
func CreateHTML(currentPage int, pages int, linksBase string) string {
|
func CreateHTML(currentPage int, pages int, linksBase string) string {
|
||||||
// Load templates.
|
// Load templates.
|
||||||
paginationHTMLRaw, err := assets.Data.ReadFile("pagination.html")
|
paginationHTMLRaw, err := static.ReadFile("pagination.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "Missing pagination.html"
|
return "Missing pagination.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
paginationLinkRaw, err1 := assets.Data.ReadFile("pagination_link.html")
|
paginationLinkRaw, err1 := static.ReadFile("pagination_link.html")
|
||||||
if err1 != nil {
|
if err1 != nil {
|
||||||
return "Missing pagination_link.html"
|
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 {
|
if err2 != nil {
|
||||||
return "Missing pagination_link_current.html"
|
return "Missing pagination_link_current.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
paginationEllipsisRaw, err3 := assets.Data.ReadFile("pagination_ellipsis.html")
|
paginationEllipsisRaw, err3 := static.ReadFile("pagination_ellipsis.html")
|
||||||
if err3 != nil {
|
if err3 != nil {
|
||||||
return "Missing pagination_ellipsis.html"
|
return "Missing pagination_ellipsis.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
// First page should always be visible.
|
// First page should always be visible.
|
||||||
var paginationString string
|
var paginationString = ""
|
||||||
if currentPage == 1 {
|
if currentPage == 1 {
|
||||||
paginationString = strings.Replace(string(paginationLinkCurrentRaw), "{pageNum}", strconv.Itoa(currentPage), -1)
|
paginationString = strings.Replace(string(paginationLinkCurrentRaw), "{pageNum}", strconv.Itoa(currentPage), -1)
|
||||||
} else {
|
} else {
|
||||||
paginationString = strings.Replace(string(paginationLinkRaw), "{pageNum}", "1", -1)
|
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 (
|
var ellipsisStartAdded = false
|
||||||
ellipsisStartAdded = false
|
var ellipsisEndAdded = false
|
||||||
ellipsisEndAdded = false
|
i := 2
|
||||||
//nolint:varnamelen
|
|
||||||
i = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
for i <= pages {
|
for i <= pages {
|
||||||
// ToDo: fix it!
|
|
||||||
//nolint:nestif
|
|
||||||
if pages > 5 {
|
if pages > 5 {
|
||||||
if currentPage-3 < i && currentPage+3 > i || i == pages {
|
if currentPage-3 < i && currentPage+3 > i || i == pages {
|
||||||
paginationItemRaw := string(paginationLinkRaw)
|
var paginationItemRaw = string(paginationLinkRaw)
|
||||||
if i == currentPage {
|
if i == currentPage {
|
||||||
paginationItemRaw = string(paginationLinkCurrentRaw)
|
paginationItemRaw = string(paginationLinkCurrentRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
paginationItem := strings.Replace(paginationItemRaw, "{pageNum}", strconv.Itoa(i), -1)
|
paginationItem := strings.Replace(paginationItemRaw, "{pageNum}", strconv.Itoa(i), -1)
|
||||||
paginationItem = strings.Replace(paginationItem, "{paginationLink}", linksBase+strconv.Itoa(i), 1)
|
paginationItem = strings.Replace(paginationItem, "{paginationLink}", linksBase+strconv.Itoa(i), 1)
|
||||||
paginationString += paginationItem
|
paginationString += paginationItem
|
||||||
|
@ -69,7 +64,7 @@ func CreateHTML(currentPage int, pages int, linksBase string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
paginationItemRaw := string(paginationLinkRaw)
|
var paginationItemRaw = string(paginationLinkRaw)
|
||||||
if i == currentPage {
|
if i == currentPage {
|
||||||
paginationItemRaw = string(paginationLinkCurrentRaw)
|
paginationItemRaw = string(paginationLinkCurrentRaw)
|
||||||
}
|
}
|
||||||
|
@ -78,7 +73,7 @@ func CreateHTML(currentPage int, pages int, linksBase string) string {
|
||||||
paginationString += paginationItem
|
paginationString += paginationItem
|
||||||
}
|
}
|
||||||
|
|
||||||
i++
|
i += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
pagination := strings.Replace(string(paginationHTMLRaw), "{paginationLinks}", paginationString, 1)
|
pagination := strings.Replace(string(paginationHTMLRaw), "{paginationLinks}", paginationString, 1)
|
449
pastes/api_http.go
Normal file
449
pastes/api_http.go
Normal file
|
@ -0,0 +1,449 @@
|
||||||
|
// 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/captcha"
|
||||||
|
"github.com/pztrn/fastpastebin/pagination"
|
||||||
|
"github.com/pztrn/fastpastebin/pastes/model"
|
||||||
|
"github.com/pztrn/fastpastebin/templater"
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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 := c.Database.GetPaste(pasteID)
|
||||||
|
if err1 != nil {
|
||||||
|
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if paste.IsExpired() {
|
||||||
|
c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 :)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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["pasteExpiration"] = paste.GetExpirationTime().Format("2006-01-02 @ 15:04:05") + " UTC"
|
||||||
|
pasteData["pasteLanguage"] = paste.Language
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
pasteData["pastedata"] = buf.String()
|
||||||
|
|
||||||
|
// Get template and format it.
|
||||||
|
pasteHTML := templater.GetTemplate(ec, "paste.html", pasteData)
|
||||||
|
|
||||||
|
return ec.HTML(http.StatusOK, pasteHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
|
||||||
|
func pastePasswordedVerifyGet(ec echo.Context) error {
|
||||||
|
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 := c.Database.GetPaste(pasteID)
|
||||||
|
if err1 != nil {
|
||||||
|
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDRaw+" not found")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML data.
|
||||||
|
htmlData := make(map[string]string)
|
||||||
|
htmlData["pasteID"] = strconv.Itoa(pasteID)
|
||||||
|
htmlData["pasteTimestamp"] = timestampRaw
|
||||||
|
|
||||||
|
verifyHTML := templater.GetTemplate(ec, "passworded_paste_verify.html", htmlData)
|
||||||
|
|
||||||
|
return ec.HTML(http.StatusOK, verifyHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
|
||||||
|
func pastePasswordedVerifyPost(ec echo.Context) error {
|
||||||
|
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 := c.Database.GetPaste(pasteID)
|
||||||
|
if err1 != nil {
|
||||||
|
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err2 := ec.FormParams()
|
||||||
|
if err2 != nil {
|
||||||
|
c.Logger.Debug().Msg("No form parameters passed")
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
|
||||||
|
return ec.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)
|
||||||
|
ec.SetCookie(cookie)
|
||||||
|
|
||||||
|
return ec.Redirect(http.StatusFound, "/paste/"+strconv.Itoa(pasteID)+"/"+timestampRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Invalid password. Please, try again.")
|
||||||
|
return ec.HTML(http.StatusBadRequest, string(errtpl))
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST for "/paste/" which will create new paste and redirect to
|
||||||
|
// "/pastes/CREATED_PASTE_ID".
|
||||||
|
func pastePOST(ec echo.Context) error {
|
||||||
|
params, err := ec.FormParams()
|
||||||
|
if err != nil {
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Cannot create empty paste")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Empty pastes aren't allowed.")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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])
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Invalid captcha solution.")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
paste := &pastesmodel.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())
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
paste.KeepFor = keepFor
|
||||||
|
|
||||||
|
keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0]
|
||||||
|
keepForUnit := pastesmodel.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 && pastePassword[0] != "" {
|
||||||
|
paste.Private = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if pastePassword[0] != "" {
|
||||||
|
paste.CreatePassword(pastePassword[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err2 := c.Database.SavePaste(paste)
|
||||||
|
if err2 != nil {
|
||||||
|
c.Logger.Debug().Msgf("Failed to save paste: %s", err2.Error())
|
||||||
|
errtpl := templater.GetErrorTemplate(ec, "Failed to save paste. Please, try again later.")
|
||||||
|
return ec.HTML(http.StatusBadRequest, errtpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := c.Database.GetPaste(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 {
|
||||||
|
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 := c.Database.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())
|
||||||
|
noPastesToShowTpl := templater.GetErrorTemplate(ec, "No pastes to show.")
|
||||||
|
return ec.HTML(http.StatusOK, noPastesToShowTpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pastes) > 0 {
|
||||||
|
pastesString = ""
|
||||||
|
for i := range pastes {
|
||||||
|
pasteDataMap := make(map[string]string)
|
||||||
|
pasteDataMap["pasteID"] = strconv.Itoa(pastes[i].ID)
|
||||||
|
pasteDataMap["pasteTitle"] = pastes[i].Title
|
||||||
|
pasteDataMap["pasteDate"] = pastes[i].CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
pasteDataMap["pasteData"] = pasteData
|
||||||
|
pasteTpl := templater.GetRawTemplate(ec, "pastelist_paste.html", pasteDataMap)
|
||||||
|
|
||||||
|
pastesString += pasteTpl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination.
|
||||||
|
pages := c.Database.GetPastesPages()
|
||||||
|
c.Logger.Debug().Msgf("Total pages: %d, current: %d", pages, page)
|
||||||
|
paginationHTML := pagination.CreateHTML(page, pages, "/pastes/")
|
||||||
|
|
||||||
|
pasteListTpl := templater.GetTemplate(ec, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML})
|
||||||
|
|
||||||
|
return ec.HTML(http.StatusOK, string(pasteListTpl))
|
||||||
|
}
|
|
@ -25,37 +25,36 @@
|
||||||
package pastes
|
package pastes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
// local
|
||||||
|
"github.com/pztrn/fastpastebin/context"
|
||||||
"go.dev.pztrn.name/fastpastebin/internal/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var regexInts = regexp.MustCompile("[0-9]+")
|
var (
|
||||||
|
c *context.Context
|
||||||
|
)
|
||||||
|
|
||||||
var ctx *context.Context
|
// New initializes pastes package and adds neccessary HTTP and API
|
||||||
|
|
||||||
// New initializes pastes package and adds necessary HTTP and API
|
|
||||||
// endpoints.
|
// endpoints.
|
||||||
func New(cc *context.Context) {
|
func New(cc *context.Context) {
|
||||||
ctx = cc
|
c = cc
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
|
||||||
// HTTP endpoints.
|
|
||||||
////////////////////////////////////////////////////////////
|
|
||||||
// New paste.
|
// New paste.
|
||||||
ctx.Echo.POST("/paste/", pastePOSTWebInterface)
|
c.Echo.POST("/paste/", pastePOST)
|
||||||
|
|
||||||
// Show public paste.
|
// Show public paste.
|
||||||
ctx.Echo.GET("/paste/:id", pasteGETWebInterface)
|
c.Echo.GET("/paste/:id", pasteGET)
|
||||||
// Show RAW representation of public paste.
|
// Show RAW representation of public paste.
|
||||||
ctx.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface)
|
c.Echo.GET("/paste/:id/raw", pasteRawGET)
|
||||||
|
|
||||||
// Show private paste.
|
// Show private paste.
|
||||||
ctx.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface)
|
c.Echo.GET("/paste/:id/:timestamp", pasteGET)
|
||||||
// Show RAW representation of private paste.
|
// 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.
|
// Verify access to passworded paste.
|
||||||
ctx.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
|
c.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
|
||||||
ctx.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
|
c.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
|
||||||
|
|
||||||
// Pastes list.
|
// Pastes list.
|
||||||
ctx.Echo.GET("/pastes/", pastesGET)
|
c.Echo.GET("/pastes/", pastesGET)
|
||||||
ctx.Echo.GET("/pastes/:page", pastesGET)
|
c.Echo.GET("/pastes/:page", pastesGET)
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user