Compare commits

...

45 Commits

Author SHA1 Message Date
Stanislav Nikitin a0f896dc53
CI config update. 2021-01-09 06:23:28 +05:00
Stanislav Nikitin fa7a79310d
Assets update. 2021-01-09 06:15:38 +05:00
Stanislav Nikitin ce962ddd00
Version bump. 2021-01-09 06:14:16 +05:00
Stanislav Nikitin 164a37d41f
A pretty basic pastes cleanup procedure.
Fixes #16.
2021-01-09 06:09:26 +05:00
Stanislav Nikitin 3ebff29113
Linting fixes about struct alignment. 2021-01-08 12:01:53 +05:00
Stanislav Nikitin 7f04a9a7bb
Typo fix and missed dependencies files. 2021-01-08 12:00:38 +05:00
Stanislav Nikitin a2a21482e2
Dependencies update and fix for line linking.
Fixes #20.
2021-01-08 11:56:35 +05:00
Stanislav Nikitin ffb42f43eb
README update. 2020-02-29 23:51:23 +05:00
Stanislav Nikitin 3f9801b1bd
Update chroma and make line numbers linkable.
Yet no links will be generated.

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

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

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

Fixed fastpastebin crash when database isn't available and added cute
error screen for informing users about such condition (both HTML and raw).
2019-03-07 08:39:12 +05:00
Stanislav Nikitin 19a3a5004c
The Huge Refactoring. 2019-03-07 07:56:50 +05:00
Stanislav Nikitin 0bf20cd2c9
Assets update. 2018-12-01 04:46:27 +05:00
Stanislav Nikitin 35c217fe46
Builder script and gitignore update. 2018-12-01 04:23:15 +05:00
Stanislav Nikitin 849c72b238
Version bump. 2018-12-01 03:55:11 +05:00
Stanislav Nikitin c565ec8f21 Fixed timezone things with postgresql and made "Forever" work. 2018-12-01 03:49:04 +05:00
Stanislav Nikitin aa5e11329f PostgreSQL support. 2018-12-01 03:23:52 +05:00
Stanislav Nikitin b6556d6e90 Moved database migrations for MySQL dialect into approriate package. 2018-12-01 02:29:14 +05:00
Stanislav Nikitin 400ce0db0c Quick and dirty fix to get database connection reestablish if connection was lost. 2018-12-01 02:23:36 +05:00
Stanislav Nikitin fc8f4e9d8b Switch to Gitlab. 2018-12-01 02:16:06 +05:00
690 changed files with 161990 additions and 23463 deletions

69
.drone.yml Normal file
View File

@ -0,0 +1,69 @@
---
kind: pipeline
type: docker
name: build
steps:
- name: notify-start
image: pztrn/discordrone
settings:
webhook_id:
from_secret: discord_webhook_id
webhook_token:
from_secret: discord_webhook_secret
message: 'Starting building **{{repo.name}}#{{build.number}}@{{commit.sha}}** @ {{datetime build.started "02-Jan-2006 15:04:05 MST" "Asia/Yekaterinburg"}} (See {{build.link}} for logs).'
- name: lint
image: golangci/golangci-lint:latest
environment:
GOFLAGS: -mod=vendor
CGO_ENABLED: 0
commands:
- golangci-lint run
depends_on:
- notify-start
- name: test
image: golang:1.13.5-alpine
environment:
GOFLAGS: -mod=vendor
CGO_ENABLED: 0
commands:
- go test ./...
depends_on:
- notify-start
- name: docker
image: plugins/docker
when:
branch: master
settings:
username:
from_secret: dockerhub_user
password:
from_secret: dockerhub_password
repo: pztrn/fastpastebin
auto_tag: true
depends_on:
- lint
- test
- name: notify-end
when:
status:
- success
- failure
image: pztrn/discordrone
settings:
webhook_id:
from_secret: discord_webhook_id
webhook_token:
from_secret: discord_webhook_secret
message: "
{{#success build.status}}
**{{repo.name}}#{{build.number}}@{{commit.sha}}** built in {{since build.startedint}} and pushed to hub.docker.com.
{{ else }}
**{{repo.name}}#{{build.number}}@{{commit.sha}}** failed. See {{build.link}}.
{{/success}}"
depends_on:
- docker

7
.gitignore vendored
View File

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

31
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,31 @@
image: docker:dind
variables:
HOST: 0.0.0.0
PORT: 2375
DOCKER_HOST: tcp://docker:2375/
DOCKER_DRIVER: overlay2
CONTAINER_NAME: registry.gitlab.pztrn.name/fastpastebin/fastpastebin
GIT_STRATEGY: clone
DOCKER_TLS_CERTDIR: ""
services:
- docker:dind
stages:
- build
before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
build:
stage: build
tags:
- docker
script:
- source docker/set_docker_tag.sh
- docker build -t $CONTAINER_NAME:$DOCKER_TAG .
- docker push $CONTAINER_NAME:$DOCKER_TAG
only:
- tags
- master

21
.golangci.yml Normal file
View File

@ -0,0 +1,21 @@
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
linters-settings:
lll:
line-length: 420
gocognit:
min-complexity: 50
gocyclo:
min-complexity: 40

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM golang:1.13.1-alpine AS build
WORKDIR /fastpastebin
COPY . .
WORKDIR /fastpastebin/cmd/fastpastebin
RUN GOFLAGS="-mod=vendor" go build
FROM alpine:3.10
LABEL maintainer "Stanislav N. <pztrn@pztrn.name>"
COPY --from=build /fastpastebin/cmd/fastpastebin/fastpastebin /app/fastpastebin
COPY docker/fastpastebin.docker.yaml /app/fastpastebin.yaml
EXPOSE 25544
ENTRYPOINT [ "/app/fastpastebin", "-config", "/app/fastpastebin.yaml" ]

187
Gopkg.lock generated
View File

@ -1,187 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/alecthomas/chroma"
packages = [
".",
"formatters",
"formatters/html",
"lexers",
"lexers/a",
"lexers/b",
"lexers/c",
"lexers/d",
"lexers/e",
"lexers/f",
"lexers/g",
"lexers/h",
"lexers/i",
"lexers/internal",
"lexers/j",
"lexers/k",
"lexers/l",
"lexers/m",
"lexers/n",
"lexers/o",
"lexers/p",
"lexers/q",
"lexers/r",
"lexers/s",
"lexers/t",
"lexers/v",
"lexers/w",
"lexers/x",
"lexers/y",
"styles"
]
revision = "3020e2ea8c6b1a9c2336022d847c4392c3997f02"
version = "v0.4.0"
[[projects]]
branch = "master"
name = "github.com/danwakefield/fnmatch"
packages = ["."]
revision = "cbb64ac3d964b81592e64f957ad53df015803288"
[[projects]]
branch = "master"
name = "github.com/dchest/captcha"
packages = ["."]
revision = "6a29415a8364ec2971fdc62d9e415ed53fc20410"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
name = "github.com/dlclark/regexp2"
packages = [
".",
"syntax"
]
revision = "487489b64fb796de2e55f4e8a4ad1e145f80e957"
version = "v1.1.6"
[[projects]]
name = "github.com/go-sql-driver/mysql"
packages = ["."]
revision = "a0583e0143b1624142adab07e0e97fe106d99561"
version = "v1.3"
[[projects]]
branch = "master"
name = "github.com/jmoiron/sqlx"
packages = [
".",
"reflectx"
]
revision = "2aeb6a910c2b94f2d5eb53d9895d80e27264ec41"
[[projects]]
name = "github.com/labstack/echo"
packages = [
".",
"middleware"
]
revision = "6d227dfea4d2e52cb76856120b3c17f758139b4e"
version = "3.3.5"
[[projects]]
name = "github.com/labstack/gommon"
packages = [
"bytes",
"color",
"log",
"random"
]
revision = "588f4e8bddc6cb45c27b448e925c7fd6a5545434"
version = "0.2.5"
[[projects]]
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
name = "github.com/pressly/goose"
packages = ["."]
revision = "056a4d47dcc4d67fa3947a4f13945a5c690e568b"
version = "v2.1.0"
[[projects]]
branch = "master"
name = "github.com/pztrn/flagger"
packages = ["."]
revision = "1330c5f1b64f253b0505ee4a2417fb8be856b87d"
[[projects]]
name = "github.com/rs/zerolog"
packages = [
".",
"internal/cbor",
"internal/json"
]
revision = "05eafee0eb17d0150591a8f30f0fa592cc9b7471"
version = "v1.6.0"
[[projects]]
branch = "master"
name = "github.com/valyala/bytebufferpool"
packages = ["."]
revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7"
[[projects]]
branch = "master"
name = "github.com/valyala/fasttemplate"
packages = ["."]
revision = "dcecefd839c4193db0d35b88ec65b4c12d360ab0"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"acme",
"acme/autocert",
"pbkdf2",
"scrypt"
]
revision = "b49d69b5da943f7ef3c9cf91c8777c1f78a0cc3c"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"webdav",
"webdav/internal/xml"
]
revision = "5f9ae10d9af5b1c89ae6904293b14b064d4ada23"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "cbbc999da32df943dac6cd71eb3ee39e1d7838b9"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "aea0cd48405b88f2c799a3d994b952758f29e06ada92b4bbe6cc4ff105d95d59"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,34 +0,0 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/rs/zerolog"
version = "1.6.0"
[prune]
go-tests = true
unused-packages = true

View File

@ -1,79 +1,60 @@
[Chat on Keybase.io](https://keybase.io/team/fastpastebin)
# Fast Pastebin
Easy-to-use-and-install pastebin software written in Go. No bells or
whistles, no websockets and even NO JAVASCRIPT!
[![Drone (self-hosted)](https://img.shields.io/drone/build/fastpastebin/fastpastebin?server=https%3A%2F%2Fci.dev.pztrn.name)](https://ci.dev.pztrn.name/fastpastebin/fastpastebin/) [![Discord](https://img.shields.io/discord/632359730089689128)](https://discord.gg/qHN6KsD) ![Keybase XLM](https://img.shields.io/keybase/xlm/pztrn)
# Current functionality.
Easy-to-use-and-install pastebin software written in Go. No bells or whistles, no websockets and even NO JAVASCRIPT!
## Current functionality
* Create and view public and private pastes.
* Syntax highlighting.
* Pastes expiration.
* Passwords for pastes.
* Multiple storage backends. Currently: ``flatfiles`` and ``mysql``.
* Multiple storage backends. Currently: ``flatfiles``, ``mysql`` and ``postgresql``.
# Caveats.
## Caveats
* No links at lines numbers. See https://github.com/alecthomas/chroma/issues/132
* No links at lines numbers. See [this Chroma bug](https://github.com/alecthomas/chroma/issues/132)
# Installation and updating
## Installation and updating
Just issue:
```
go get -u -v github.com/pztrn/fastpastebin/cmd/fastpastebin
```bash
CGO_ENABLED=0 go get -u -v go.dev.pztrn.name/fastpastebin/cmd/fastpastebin
```
This command can be used to update Fast Paste Bin.
**WARNING:** installation by compiling Fast Paste Bin from sources **require**
at least 300 megabytes of free RAM! Eventually it'll run even on 64MB-powered
VM, it's only a compilation issue.
## Configuration
# Configuration.
Take a look at [example configuration file](examples/fastpastebin.yaml.dist) which contains all supported options and their descriptions.
Take a look at [example configuration file](examples/fastpastebin.yaml.dist)
which contains all supported options and their descriptions.
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.
Configuration file position is irrelevant, there is no hardcoded paths where
Fast Paste Bin looking for it's configuration. Use ``-config`` CLI parameter
or ``FASTPASTEBIN_CONFIG`` environment variable to specify path.
## Developing
# Developing
Developers should install [fileb0x](https://github.com/UnnoTed/fileb0x/) which is used as replacement to go-bindata for embedding assets into binary. After changing assets they should be recompiled into Go code. At repository root execute this command and you'll be fine:
Developers should install https://github.com/UnnoTed/fileb0x/ which is used
as replacement to go-bindata for embedding assets into binary. After changing
assets they should be recompiled into Go code. At repository root execute
this command and you'll be fine:
```
```bash
fileb0x fileb0x.yml
```
Also if you're changed list of assets (by creating or deleting them) be sure
to fix files list in ``fileb0x.yml`` file!
Also if you're changed list of assets (by creating or deleting them) be sure to fix files list in ``fileb0x.yml`` file!
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:
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 [Go's code review comments](https://github.com/golang/go/wiki/CodeReviewComments) with few exceptions:
* Imports should be organized in 3 groups: stdlib, local, other. See
https://github.com/pztrn/fastpastebin/blob/master/pastes/api_http.go for
example.
* Imports should be organized in 3 groups: stdlib, local, other. See [this file](https://sources.dev.pztrn.name/fastpastebin/fastpastebin/src/branch/master/domains/pastes/paste_get.go) for example.
* We're not forcing any limits on line length for code, only for comments, they should be 72-76 chars long.
* We're not forcing any limits on line length for code, only for comments,
they should be 72-76 chars long.
## ToDo
# ToDo
This is a ToDo list which isn't sorted by any parameter at all. Just a list
of tasks you can help with.
This is a ToDo list which isn't sorted by any parameter at all. Just a list of tasks you can help with.
* User CP.
* Files uploading.
* Passwords for files.
* Pastes forking and revisioning (like git or github gists).
* Possibility to copy-paste-edit WISYWIG content.
* CLI client for pastes and files uploading.
* Possibility to copy-paste-edit WYSIWYG content.
* CLI client for pastes and files uploading.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
assets/css/bulma-0.7.5.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,9 @@
.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
}

View File

@ -0,0 +1,6 @@
<section class="section">
<div class="notification is-danger">
<h3><strong>Database not available</strong></h3>
<p>Something went wrong while trying to connect to database. Check logs for details.</p>
</div>
</section>

View File

@ -5,7 +5,7 @@
<strong>{version}</strong> by
<a href="https://pztrn.name">Stanislav N. aka pztrn</a>. The source code is licensed
<a href="http://opensource.org/licenses/mit-license.php">MIT</a>. Get
<a href="https://github.com/pztrn/fastpastebin">source or binary releases here</a>!
<a href="https://gitlab.pztrn.name/fastpastebin/fastpastebin">source or binary releases here</a>!
</p>
</div>
</div>
</div>

View File

@ -44,8 +44,7 @@
</label>
</div>
<div>OR</div>
<div class="field tooltip is-tooltip-bottom is-tooltip-multiline" data-tooltip="If you'll enter password here - 'Private
paste with unique URL' checkbox will be assumed as checked.">
<div class="field tooltip is-tooltip-bottom is-tooltip-multiline" data-tooltip="If you'll enter password here - 'Private paste with unique URL' checkbox will be assumed as checked.">
<label for="paste-password">Password for paste:</label>
<div class="control">
<input class="input" type="text" placeholder="Enter password." name="paste-password" id="paste-password">

View File

@ -5,8 +5,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fast Paste Bin</title>
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
<link rel="stylesheet" href="/static/css/bulma-tooltip-1.0.4.min.css">
<link rel="stylesheet" href="/static/css/bulma-0.7.5.min.css">
<link rel="stylesheet" href="/static/css/bulma-tooltip-3.0.0.min.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>

View File

@ -1,5 +1,5 @@
// 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)
// Code generated by fileb0x at "2021-01-09 06:15:34.328599857 +0500 +05 m=+0.033228523" from config file "fileb0x.yml" DO NOT EDIT.
// modification hash(a501d12d9fe3316e4b2134554bdc730c.238872c2653234e79053054d1ba776af)
package static
@ -7,6 +7,7 @@ package static
import (
"bytes"
"context"
"io"
"net/http"
"os"
@ -14,7 +15,6 @@ import (
"golang.org/x/net/webdav"
"context"
)
@ -36,13 +36,17 @@ var (
)
// HTTPFS implements http.FileSystem
type HTTPFS struct {}
type HTTPFS struct {
// Prefix allows to limit the path of all requests. F.e. a prefix "css" would allow only calls to /css/*
Prefix string
}
func init() {
if CTX.Err() != nil {
panic(CTX.Err())
err := CTX.Err()
if err != nil {
panic(err)
}
@ -50,10 +54,6 @@ func init() {
var err error
err = FS.Mkdir(CTX, "static/", 0777)
if err != nil && err != os.ErrExist {
@ -91,6 +91,7 @@ func init() {
// Open a file
func (hfs *HTTPFS) Open(path string) (http.File, error) {
path = hfs.Prefix + path
f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644)

View File

@ -0,0 +1,35 @@
// Code generaTed by fileb0x at "2021-01-09 06:15:34.380518788 +0500 +05 m=+0.085147493" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.805743275 +0500 +05)
// original path: assets/database_not_available.html
package static
import (
"os"
)
// FileDatabaseNotAvailableHTML is "/database_not_available.html"
var FileDatabaseNotAvailableHTML = []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\x6e\x6f\x74\x69\x66\x69\x63\x61\x74\x69\x6f\x6e\x20\x69\x73\x2d\x64\x61\x6e\x67\x65\x72\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x68\x33\x3e\x3c\x73\x74\x72\x6f\x6e\x67\x3e\x44\x61\x74\x61\x62\x61\x73\x65\x20\x6e\x6f\x74\x20\x61\x76\x61\x69\x6c\x61\x62\x6c\x65\x3c\x2f\x73\x74\x72\x6f\x6e\x67\x3e\x3c\x2f\x68\x33\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x3e\x53\x6f\x6d\x65\x74\x68\x69\x6e\x67\x20\x77\x65\x6e\x74\x20\x77\x72\x6f\x6e\x67\x20\x77\x68\x69\x6c\x65\x20\x74\x72\x79\x69\x6e\x67\x20\x74\x6f\x20\x63\x6f\x6e\x6e\x65\x63\x74\x20\x74\x6f\x20\x64\x61\x74\x61\x62\x61\x73\x65\x2e\x20\x43\x68\x65\x63\x6b\x20\x6c\x6f\x67\x73\x20\x66\x6f\x72\x20\x64\x65\x74\x61\x69\x6c\x73\x2e\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, "/database_not_available.html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
if err != nil {
panic(err)
}
_, err = f.Write(FileDatabaseNotAvailableHTML)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
}

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.366001707 +0500 +05 m=+0.070630336" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.805743275 +0500 +05)
// original path: assets/error.html
package static

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.379885693 +0500 +05 m=+0.084514363" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-09 06:13:46.102341215 +0500 +05)
// original path: assets/footer.html
package static
@ -10,7 +10,7 @@ import (
)
// 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")
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\x6c\x61\x62\x2e\x70\x7a\x74\x72\x6e\x2e\x6e\x61\x6d\x65\x2f\x66\x61\x73\x74\x70\x61\x73\x74\x65\x62\x69\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\x0a")
func init() {

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.333846038 +0500 +05 m=+0.038474816" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/main.html
package static
@ -10,7 +10,7 @@ import (
)
// 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")
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\x35\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\x33\x2e\x30\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\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() {

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.381029312 +0500 +05 m=+0.085658023" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/navigation.html
package static

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.373359597 +0500 +05 m=+0.077988268" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/pagination.html
package static

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.372861545 +0500 +05 m=+0.077490249" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/pagination_ellipsis.html
package static

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.334873281 +0500 +05 m=+0.039502074" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/pagination_link.html
package static

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.366696762 +0500 +05 m=+0.071325415" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/pagination_link_current.html
package static

View File

@ -1,5 +1,5 @@
// Code generaTed by fileb0x at "2018-05-26 13:49:35.048457009 +0500 +05 m=+0.011636574" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2018-05-26 13:49:28.988992677 +0500 +05)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.373841671 +0500 +05 m=+0.078470340" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/passworded_paste_verify.html
package static

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.36723926 +0500 +05 m=+0.071867934" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/paste.html
package static

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.335668233 +0500 +05 m=+0.040296968" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/pastelist_list.html
package static

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.336449591 +0500 +05 m=+0.041078378" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/pastelist_paste.html
package static

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
// 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)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.37218288 +0500 +05 m=+0.076811562" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.805743275 +0500 +05)
// original path: assets/css/style.css
package static
@ -10,7 +10,7 @@ import (
)
// 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")
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\x0a\x0a\x2f\x2a\x20\x54\x68\x69\x73\x20\x69\x73\x20\x61\x20\x73\x70\x65\x63\x69\x61\x6c\x20\x63\x61\x73\x65\x20\x66\x6f\x72\x20\x6d\x75\x6c\x74\x69\x6c\x69\x6e\x65\x20\x74\x6f\x6f\x6c\x74\x69\x70\x73\x2e\x20\x53\x65\x65\x20\x68\x74\x74\x70\x73\x3a\x2f\x2f\x67\x69\x74\x68\x75\x62\x2e\x63\x6f\x6d\x2f\x57\x69\x6b\x69\x6b\x69\x2f\x62\x75\x6c\x6d\x61\x2d\x74\x6f\x6f\x6c\x74\x69\x70\x2f\x69\x73\x73\x75\x65\x73\x2f\x33\x34\x20\x2a\x2f\x0a\x2e\x74\x6f\x6f\x6c\x74\x69\x70\x2e\x69\x73\x2d\x74\x6f\x6f\x6c\x74\x69\x70\x2d\x6d\x75\x6c\x74\x69\x6c\x69\x6e\x65\x3a\x3a\x62\x65\x66\x6f\x72\x65\x20\x7b\x0a\x20\x20\x20\x20\x77\x68\x69\x74\x65\x2d\x73\x70\x61\x63\x65\x3a\x70\x72\x65\x2d\x6c\x69\x6e\x65\x0a\x7d")
func init() {

View File

@ -1,5 +1,5 @@
// Code generaTed by fileb0x at "2018-04-30 12:11:40.314331472 +0500 +05 m=+0.031685082" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2018-02-27 01:53:44 +0500 +05)
// Code generaTed by fileb0x at "2021-01-09 06:15:34.33997584 +0500 +05 m=+0.044604593" from config file "fileb0x.yml" DO NOT EDIT.
// modified(2021-01-08 11:51:03.815743367 +0500 +05)
// original path: assets/js/fontawesome-5.0.7.js
package static

26
builder.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
VERSION=$1
if [ "${VERSION}" == "" ]; then
echo "Specify version as first parameter!"
exit 1
fi
OS_LIST=("darwin/amd64" "dragonfly/amd64" "freebsd/386" "freebsd/amd64" "freebsd/arm" "linux/386" "linux/amd64" "linux/arm" "linux/arm64" "linux/ppc64" "linux/ppc64le" "linux/mips" "linux/mipsle" "linux/mips64" "linux/mips64le" "linux/s390x" "netbsd/386" "netbsd/amd64" "netbsd/arm" "openbsd/386" "openbsd/amd64" "openbsd/arm" "solaris/amd64" "windows/386" "windows/amd64")
if [ ! -d ./dist ]; then
mkdir -p ./dist
fi
for os in ${OS_LIST[@]}; do
mkdir -p ./dist/${os}
goos=$(echo ${os} | awk -F"/" '{ print $1 }')
goarch=$(echo ${os} | awk -F"/" '{ print $2 }')
echo "Building for ${goos} ${goarch}..."
GOOS=${goos} GOARCH=${goarch} GOFLAGS="-mod=vendor" go build -o ./dist/${os}/fastpastebin ./cmd/fastpastebin/
cp ./examples/fastpastebin.yaml.dist ./dist/${os}/fastpastebin.yaml
cd ./dist/${os}/
tar -czf fastpastebin-${VERSION}-${goos}-${goarch}.tar.gz fastpastebin fastpastebin.yaml
mv fastpastebin-${VERSION}-${goos}-${goarch}.tar.gz ../../
cd - &>/dev/null
done

View File

@ -31,13 +31,13 @@ import (
"syscall"
// local
"github.com/pztrn/fastpastebin/api"
"github.com/pztrn/fastpastebin/captcha"
"github.com/pztrn/fastpastebin/context"
"github.com/pztrn/fastpastebin/database"
"github.com/pztrn/fastpastebin/database/migrations"
"github.com/pztrn/fastpastebin/pastes"
"github.com/pztrn/fastpastebin/templater"
"go.dev.pztrn.name/fastpastebin/domains/dbnotavailable"
"go.dev.pztrn.name/fastpastebin/domains/indexpage"
"go.dev.pztrn.name/fastpastebin/domains/pastes"
"go.dev.pztrn.name/fastpastebin/internal/captcha"
"go.dev.pztrn.name/fastpastebin/internal/context"
"go.dev.pztrn.name/fastpastebin/internal/database"
"go.dev.pztrn.name/fastpastebin/internal/templater"
)
func main() {
@ -54,21 +54,23 @@ func main() {
// Continue loading.
c.LoadConfiguration()
c.InitializePost()
database.New(c)
c.Database.Initialize()
migrations.New(c)
migrations.Migrate()
templater.Initialize(c)
api.New(c)
api.InitializeAPI()
captcha.New(c)
dbnotavailable.New(c)
indexpage.New(c)
pastes.New(c)
// CTRL+C handler.
signalHandler := make(chan os.Signal, 1)
shutdownDone := make(chan bool, 1)
signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM)
go func() {
<-signalHandler
c.Shutdown()

37
docker-compose.yml Normal file
View File

@ -0,0 +1,37 @@
version: "3"
volumes:
mysql:
services:
database:
image: mariadb:10.3
container_name: database
restart: always
volumes:
- fastpastebin_mysql:/var/lib/mysql
environment:
- MYSQL_RANDOM_ROOT_PASSWORD=yes
- MYSQL_DATABASE=fastpastebin
- MYSQL_USER=fastpastebin
- MYSQL_PASSWORD=fastpastebin
fastpastebin:
build: .
image: fastpastebin
container_name: fastpastebin
restart: always
depends_on:
- database
volumes:
- ./docker/fastpastebin.docker.yaml:/app/fastpastebin.yaml:ro
# ports:
# - 25544:25544
web:
image: nginx:1.16-alpine
container_name: nginx
volumes:
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- 8080:80
depends_on:
- fastpastebin

View File

@ -0,0 +1,22 @@
database:
type: "mysql"
path: "./data"
address: "database"
port: "3306"
username: "fastpastebin"
password: "fastpastebin"
database: "fastpastebin"
logging:
log_to_file: false
filename: ""
# Log level. Acceptable parameters: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
loglevel: "DEBUG"
http:
address: "0.0.0.0"
port: "25544"
allow_insecure: true
pastes:
pagination: 10

10
docker/nginx.conf Normal file
View File

@ -0,0 +1,10 @@
server {
listen 80 default_server;
location / {
proxy_pass http://fastpastebin:25544;
proxy_set_header Host $Host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

7
docker/set_docker_tag.sh Normal file
View File

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

View File

@ -22,30 +22,26 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package http
package dbnotavailable
import (
// stdlib
"net/http"
// local
"github.com/pztrn/fastpastebin/api/http/static"
"github.com/pztrn/fastpastebin/context"
"go.dev.pztrn.name/fastpastebin/internal/templater"
// other
"github.com/labstack/echo"
)
var (
c *context.Context
)
// Database not available error page
func dbNotAvailableGet(ec echo.Context) error {
htmlData := templater.GetTemplate(ec, "database_not_available.html", nil)
// New initializes basic HTTP API, which shows only index page and serves
// static files.
func New(cc *context.Context) {
c = cc
c.Logger.Info().Msg("Initializing HTTP API...")
// Static files.
c.Echo.GET("/static/*", echo.WrapHandler(static.Handler))
// Index.
c.Echo.GET("/", indexGet)
return ec.HTML(http.StatusInternalServerError, htmlData)
}
func dbNotAvailableRawGet(ec echo.Context) error {
return ec.String(http.StatusInternalServerError, "Database not available\nSomething went wrong while trying to connect to database. Check logs for details.")
}

View File

@ -0,0 +1,43 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package dbnotavailable
import (
// local
"go.dev.pztrn.name/fastpastebin/internal/context"
)
var (
c *context.Context
)
// New initializes pastes package and adds necessary HTTP and API
// endpoints.
func New(cc *context.Context) {
c = cc
c.Echo.GET("/database_not_available", dbNotAvailableGet)
c.Echo.GET("/database_not_available/raw", dbNotAvailableRawGet)
}

View File

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

View File

@ -22,15 +22,15 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package http
package indexpage
import (
// stdlib
"net/http"
// local
"github.com/pztrn/fastpastebin/captcha"
"github.com/pztrn/fastpastebin/templater"
"go.dev.pztrn.name/fastpastebin/internal/captcha"
"go.dev.pztrn.name/fastpastebin/internal/templater"
// other
"github.com/alecthomas/chroma/lexers"
@ -39,6 +39,12 @@ import (
// Index of this site.
func indexGet(ec echo.Context) error {
// We should check if database connection available.
dbConn := c.Database.GetDatabaseConnection()
if c.Config.Database.Type != "flatfiles" && dbConn == nil {
return ec.Redirect(http.StatusFound, "/database_not_available")
}
// Generate list of available languages to highlight.
availableLexers := lexers.Names(false)

View File

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

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

@ -0,0 +1,337 @@
package pastes
import (
// stdlib
"bytes"
"net/http"
"strconv"
"time"
// local
"go.dev.pztrn.name/fastpastebin/internal/structs"
"go.dev.pztrn.name/fastpastebin/internal/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/labstack/echo"
)
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 := c.Database.GetPaste(pasteID)
if err1 != nil {
c.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste")
return nil, pasteNotFound
}
// Check if paste is expired.
if paste.IsExpired() {
c.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 {
c.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(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])
pasteIDStr := strconv.Itoa(pasteID)
c.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 := ec.Param("timestamp")
if tsProvidedStr != "" {
tsProvided, err := strconv.ParseInt(tsProvidedStr, 10, 64)
if err != nil {
c.Logger.Error().Err(err).Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Msg("Invalid timestamp provided for getting private paste")
errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDStr+" not found")
return ec.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 := ec.Cookie("PASTE-" + pasteIDStr)
if err1 == nil {
cookieValue = cookie.Value
}
paste, error := pasteGetData(pasteID, timestamp, cookieValue)
// For these cases we should return 404 Not Found page.
if error == pasteExpired || error == pasteNotFound || error == pasteTimestampInvalid {
errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDRaw+" not found")
return ec.HTML(http.StatusNotFound, errtpl)
}
// If passed cookie value was invalid - go to paste authorization
// page.
if error == pasteCookieInvalid {
c.Logger.Info().Int("paste ID", pasteID).Msg("Invalid cookie, redirecting to auth page")
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDStr+"/"+ec.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 {
c.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 := chroma.Formatter(htmlfmt.New(htmlfmt.WithLineNumbers(true), htmlfmt.LineNumbersInTable(true), htmlfmt.LinkableLineNumbers(true, "L")))
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().Err(err4).Msg("Failed to format paste data")
}
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().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste data")
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 {
// We should check if database connection available.
dbConn := c.Database.GetDatabaseConnection()
if c.Config.Database.Type != "flatfiles" && dbConn == nil {
return ec.Redirect(http.StatusFound, "/database_not_available")
}
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().Int("paste ID", pasteID).Msg("Requesting paste")
// Get paste.
paste, err1 := c.Database.GetPaste(pasteID)
if err1 != nil {
c.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste")
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, errtpl)
}
// GET for "/pastes/:id/raw", raw paste output.
// Web interface version.
func pasteRawGETWebInterface(ec echo.Context) error {
// We should check if database connection available.
dbConn := c.Database.GetDatabaseConnection()
if c.Config.Database.Type != "flatfiles" && dbConn == nil {
return ec.Redirect(http.StatusFound, "/database_not_available/raw")
}
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().Int("paste ID", pasteID).Msg("Requesting paste data")
// Get paste.
paste, err1 := c.Database.GetPaste(pasteID)
if err1 != nil {
c.Logger.Error().Err(err1).Int("paste ID", pasteID).Msg("Failed to get paste from database")
return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
}
if paste.IsExpired() {
c.Logger.Error().Int("paste ID", pasteID).Msg("Paste is expired")
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().Err(err2).Int("paste ID", pasteID).Str("provided timestamp", tsProvidedStr).Msg("Invalid timestamp provided for getting private paste")
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
}
pasteTs := paste.CreatedAt.Unix()
if tsProvided != pasteTs {
c.Logger.Error().Int("paste ID", pasteID).Int64("provided timestamp", tsProvided).Int64("paste timestamp", pasteTs).Msg("Incorrect timestamp provided for private paste")
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
}
}
// nolint
// ToDo: figure out how to handle passworded pastes here.
// Return error for now.
if paste.Password != "" {
c.Logger.Error().Int("paste ID", pasteID).Msg("Cannot render paste as raw: passworded paste. Patches welcome!")
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
}
return ec.String(http.StatusOK, paste.Data)
}

View File

@ -0,0 +1,153 @@
package pastes
import (
// stdlib
"net/http"
"regexp"
"strconv"
"strings"
"time"
// local
"go.dev.pztrn.name/fastpastebin/internal/captcha"
"go.dev.pztrn.name/fastpastebin/internal/structs"
"go.dev.pztrn.name/fastpastebin/internal/templater"
// other
"github.com/alecthomas/chroma/lexers"
"github.com/labstack/echo"
)
// 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(ec echo.Context) error {
// We should check if database connection available.
dbConn := c.Database.GetDatabaseConnection()
if c.Config.Database.Type != "flatfiles" && dbConn == nil {
return ec.Redirect(http.StatusFound, "/database_not_available")
}
params, err := ec.FormParams()
if err != nil {
c.Logger.Error().Msg("Passed paste form is empty")
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") && params["paste-keep-for"][0] != "forever" {
c.Logger.Debug().Str("field value", params["paste-keep-for"][0]).Msg("'Keep paste for' field have invalid value")
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().Str("captcha ID", params["paste-captcha-id"][0]).Str("captcha solution", params["paste-captcha-solution"][0]).Msg("Invalid captcha solution")
errtpl := templater.GetErrorTemplate(ec, "Invalid captcha solution.")
return ec.HTML(http.StatusBadRequest, errtpl)
}
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] != "forever" {
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] == "forever" {
c.Logger.Debug().Msg("Keeping paste forever!")
keepFor = 0
} else {
c.Logger.Debug().Err(err).Msg("Failed to parse 'Keep for' integer")
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)
}
}
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])
}
id, err2 := c.Database.SavePaste(paste)
if err2 != nil {
c.Logger.Error().Err(err2).Msg("Failed to save paste")
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().Msg("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)
}

View File

@ -0,0 +1,111 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package pastes
import (
// stdlib
"net/http"
"strconv"
"strings"
// local
"go.dev.pztrn.name/fastpastebin/internal/pagination"
"go.dev.pztrn.name/fastpastebin/internal/templater"
// other
"github.com/labstack/echo"
)
// GET for "/pastes/", a list of publicly available pastes.
// Web interface version.
func pastesGET(ec echo.Context) error {
// We should check if database connection available.
dbConn := c.Database.GetDatabaseConnection()
if c.Config.Database.Type != "flatfiles" && dbConn == nil {
return ec.Redirect(http.StatusFound, "/database_not_available")
}
pageFromParamRaw := ec.Param("page")
var page = 1
if pageFromParamRaw != "" {
pageRaw := regexInts.FindAllString(pageFromParamRaw, 1)[0]
page, _ = strconv.Atoi(pageRaw)
}
c.Logger.Debug().Int("page", page).Msg("Requested page")
// Get pastes IDs.
pastes, err3 := c.Database.GetPagedPastes(page)
c.Logger.Debug().Int("count", len(pastes)).Msg("Got pastes")
var pastesString = "No pastes to show."
// Show "No pastes to show" on any error for now.
if err3 != nil {
c.Logger.Error().Err(err3).Msg("Failed to get pastes list from database")
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 string
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().Int("total pages", pages).Int("current page", page).Msg("Paging data")
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, pasteListTpl)
}

View File

@ -2,9 +2,9 @@
# Only MySQL database and flatfiles are supported for now.
database:
# Database type. The only supported ATM is "mysql" and "flatfiles".
type: "mysql"
type: "flatfiles"
# Path for data stored with "flatfiles" database adapter.
# Will be comletely ignored for MySQL/MariaDB.
# Will be completely ignored for MySQL/MariaDB.
path: "./data"
# Next parameters are strictly for MySQL/MariaDB connections and
# will be ignored by "flatfiles" adapter.
@ -25,12 +25,14 @@ logging:
# HTTP server configuration.
http:
address: "192.168.0.14"
address: "127.0.0.1"
port: "25544"
# By default we're allowing only HTTPS requests. Setting this to true
# will allow HTTP requests. Useful for developing or if you're
# running Fast Pastebin behind reverse proxy that does SSL termination.
allow_insecure: true
# Maximum body size in megabytes. 1 should be enough for most use cases.
max_body_size_megabytes: 1
# Pastes configuration.
pastes:

View File

@ -5,7 +5,7 @@
pkg: static
# destination
dest: "./api/http/static/"
dest: "./assets/static/"
# gofmt
# type: bool
@ -79,8 +79,8 @@ 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-0.7.5.min.css"
- "assets/css/bulma-tooltip-3.0.0.min.css"
- "assets/css/bulma.css.map"
- "assets/css/style.css"
- "assets/js/fontawesome-5.0.7.js"
@ -94,6 +94,7 @@ custom:
prefix: "static/"
# end: files
- files:
- "assets/database_not_available.html"
- "assets/error.html"
- "assets/footer.html"
- "assets/index.html"

21
go.mod Normal file
View File

@ -0,0 +1,21 @@
module go.dev.pztrn.name/fastpastebin
go 1.13
require (
github.com/alecthomas/chroma v0.8.2
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/go-sql-driver/mysql v1.5.0
github.com/jmoiron/sqlx v1.2.0
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.3.0 // indirect
github.com/lib/pq v1.9.0
github.com/pressly/goose v2.6.0+incompatible
github.com/rs/zerolog v1.20.0
go.dev.pztrn.name/flagger v0.0.0-20200617193309-89bc9818b76c
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/net v0.0.0-20201224014010-6772e930b67b
gopkg.in/yaml.v2 v2.4.0
)

107
go.sum Normal file
View File

@ -0,0 +1,107 @@
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg=
github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
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 v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
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.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
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.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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.6.0+incompatible h1:3f8zIQ8rfgP9tyI0Hmcs2YNAqUCL1c+diLe3iU8Qd/k=
github.com/pressly/goose v2.6.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/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.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
go.dev.pztrn.name/flagger v0.0.0-20200617193309-89bc9818b76c h1:+GgFefaTLsYDS0lXc8LNzTo6tcsA9qO3EkTAKduPAsI=
go.dev.pztrn.name/flagger v0.0.0-20200617193309-89bc9818b76c/go.mod h1:ttPExQNCubgqqO5Y19LfIBKqmWtBocY7P9MXQEECuZo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

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

View File

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

View File

@ -24,9 +24,10 @@
package config
// ConfigHTTP describes HTTP server configuration.
type ConfigHTTP struct {
Address string `yaml:"address"`
Port string `yaml:"port"`
AllowInsecure bool `yaml:"allow_insecure"`
// HTTP describes HTTP server configuration.
type HTTP struct {
Address string `yaml:"address"`
Port string `yaml:"port"`
AllowInsecure bool `yaml:"allow_insecure"`
MaxBodySizeMegabytes string `yaml:"max_body_size_megabytes"`
}

View File

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

View File

@ -24,7 +24,7 @@
package config
// ConfigPastes describes pastes subsystem configuration.
type ConfigPastes struct {
// Pastes describes pastes subsystem configuration.
type Pastes struct {
Pagination int `yaml:"pagination"`
}

View File

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

View File

@ -31,13 +31,13 @@ import (
"path/filepath"
// local
"github.com/pztrn/fastpastebin/config"
"github.com/pztrn/fastpastebin/database/interface"
"go.dev.pztrn.name/fastpastebin/internal/config"
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
// other
"github.com/labstack/echo"
"github.com/pztrn/flagger"
"github.com/rs/zerolog"
"go.dev.pztrn.name/flagger"
"gopkg.in/yaml.v2"
)
@ -46,7 +46,7 @@ import (
// contains everything every part of application need, like configuration
// access, logger, etc.
type Context struct {
Config *config.ConfigStruct
Config *config.Struct
Database databaseinterface.Interface
Echo *echo.Echo
Flagger *flagger.Flagger
@ -55,12 +55,12 @@ type Context struct {
// Initialize initializes context.
func (c *Context) Initialize() {
c.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
c.initializeLogger()
c.Flagger = flagger.New(nil)
c.Flagger = flagger.New("fastpastebin", nil)
c.Flagger.Initialize()
c.Flagger.AddFlag(&flagger.Flag{
_ = c.Flagger.AddFlag(&flagger.Flag{
Name: "config",
Description: "Configuration file path. Can be overridded with FASTPASTEBIN_CONFIG environment variable (this is what used in tests).",
Type: "string",
@ -68,13 +68,19 @@ func (c *Context) Initialize() {
})
}
// InitializePost initializes everything that needs a configuration.
func (c *Context) InitializePost() {
c.initializeLoggerPost()
c.initializeHTTPServer()
}
// LoadConfiguration loads configuration and executes right after Flagger
// have parsed CLI flags, because it depends on "-config" defined in
// Initialize().
func (c *Context) LoadConfiguration() {
c.Logger.Info().Msg("Loading configuration...")
var configPath = ""
configPath := ""
// We're accepting configuration path from "-config" CLI parameter
// and FASTPASTEBIN_CONFIG environment variable. Later have higher
@ -93,44 +99,27 @@ func (c *Context) LoadConfiguration() {
// Normalize file path.
normalizedConfigPath, err1 := filepath.Abs(configPath)
if err1 != nil {
c.Logger.Fatal().Msgf("Failed to normalize path to configuration file: %s", err1.Error())
c.Logger.Fatal().Err(err1).Msg("Failed to normalize path to configuration file")
}
c.Logger.Debug().Msgf("Configuration file path: %s", configPath)
c.Logger.Debug().Str("path", configPath).Msg("Configuration file path")
c.Config = &config.ConfigStruct{}
c.Config = &config.Struct{}
// Read configuration file.
fileData, err2 := ioutil.ReadFile(normalizedConfigPath)
if err2 != nil {
c.Logger.Panic().Msgf("Failed to read configuration file: %s", err2.Error())
c.Logger.Panic().Err(err2).Msg("Failed to read configuration file")
}
// Parse it into structure.
err3 := yaml.Unmarshal(fileData, c.Config)
if err3 != nil {
c.Logger.Panic().Msgf("Failed to parse configuration file: %s", err3.Error())
c.Logger.Panic().Err(err3).Msg("Failed to parse configuration file")
}
// Yay! See what it gets!
c.Logger.Debug().Msgf("Parsed configuration: %+v", c.Config)
// Set log level.
c.Logger.Info().Msgf("Setting logger level: %s", c.Config.Logging.LogLevel)
switch c.Config.Logging.LogLevel {
case "DEBUG":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "INFO":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "WARN":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "ERROR":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case "FATAL":
zerolog.SetGlobalLevel(zerolog.FatalLevel)
case "PANIC":
zerolog.SetGlobalLevel(zerolog.PanicLevel)
}
}
// RegisterDatabaseInterface registers database interface for later use.

View File

@ -26,7 +26,7 @@ package context
const (
// Version .
Version = "0.2.0"
Version = "0.4.0"
)
// New creates new context.

View File

@ -0,0 +1,47 @@
package context
import (
// local
"go.dev.pztrn.name/fastpastebin/assets/static"
// other
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
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(static.Handler))
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(ec echo.Context) error {
c.Logger.Info().
Str("IP", ec.RealIP()).
Str("Host", ec.Request().Host).
Str("Method", ec.Request().Method).
Str("Path", ec.Request().URL.Path).
Str("UA", ec.Request().UserAgent()).
Msg("HTTP request")
return next(ec)
}
}
}

View File

@ -0,0 +1,80 @@
package context
import (
// stdlib
"fmt"
"os"
"runtime"
"strings"
"time"
// other
"github.com/rs/zerolog"
)
// Puts memory usage into log lines.
func (c *Context) getMemoryUsage(e *zerolog.Event, level zerolog.Level, message string) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
e.Str("memalloc", fmt.Sprintf("%dMB", m.Alloc/1024/1024))
e.Str("memsys", fmt.Sprintf("%dMB", m.Sys/1024/1024))
e.Str("numgc", fmt.Sprintf("%d", m.NumGC))
}
// Initializes logger.
func (c *Context) initializeLogger() {
// Устанавливаем форматирование логгера.
output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false, TimeFormat: time.RFC3339}
output.FormatLevel = func(i interface{}) string {
var v string
if ii, ok := i.(string); ok {
ii = strings.ToUpper(ii)
switch ii {
case "DEBUG":
v = fmt.Sprintf("\x1b[30m%-5s\x1b[0m", ii)
case "ERROR":
v = fmt.Sprintf("\x1b[31m%-5s\x1b[0m", ii)
case "FATAL":
v = fmt.Sprintf("\x1b[35m%-5s\x1b[0m", ii)
case "INFO":
v = fmt.Sprintf("\x1b[32m%-5s\x1b[0m", ii)
case "PANIC":
v = fmt.Sprintf("\x1b[36m%-5s\x1b[0m", ii)
case "WARN":
v = fmt.Sprintf("\x1b[33m%-5s\x1b[0m", ii)
default:
v = ii
}
}
return fmt.Sprintf("| %s |", v)
}
c.Logger = zerolog.New(output).With().Timestamp().Logger()
c.Logger = c.Logger.Hook(zerolog.HookFunc(c.getMemoryUsage))
}
// Initialize logger after configuration parse.
func (c *Context) initializeLoggerPost() {
// Set log level.
c.Logger.Info().Msgf("Setting logger level: %s", c.Config.Logging.LogLevel)
switch c.Config.Logging.LogLevel {
case "DEBUG":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "INFO":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "WARN":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "ERROR":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case "FATAL":
zerolog.SetGlobalLevel(zerolog.FatalLevel)
case "PANIC":
zerolog.SetGlobalLevel(zerolog.PanicLevel)
}
}

View File

@ -27,12 +27,14 @@ package database
import (
// stdlib
"database/sql"
"time"
// 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"
"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"
// other
_ "github.com/go-sql-driver/mysql"
@ -43,6 +45,46 @@ 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 {
c.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 {
c.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 {
c.Logger.Error().Err(err).Int("paste", pasteID).Msg("Failed to delete paste!")
}
}
c.Logger.Info().Msg("Pastes cleanup done.")
time.Sleep(time.Hour)
}
}
func (db *Database) DeletePaste(pasteID int) error {
return db.db.DeletePaste(pasteID)
}
func (db *Database) GetDatabaseConnection() *sql.DB {
if db.db != nil {
return db.db.GetDatabaseConnection()
@ -51,11 +93,11 @@ func (db *Database) GetDatabaseConnection() *sql.DB {
return nil
}
func (db *Database) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
func (db *Database) GetPaste(pasteID int) (*structs.Paste, error) {
return db.db.GetPaste(pasteID)
}
func (db *Database) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) {
return db.db.GetPagedPastes(page)
}
@ -71,9 +113,13 @@ func (db *Database) Initialize() {
mysql.New(c)
} else if c.Config.Database.Type == "flatfiles" {
flatfiles.New(c)
} else if c.Config.Database.Type == "postgresql" {
postgresql.New(c)
} else {
c.Logger.Fatal().Msgf("Unknown database type: %s", c.Config.Database.Type)
c.Logger.Fatal().Str("type", c.Config.Database.Type).Msg("Unknown database type")
}
go db.cleanup()
}
func (db *Database) RegisterDialect(di dialectinterface.Interface) {
@ -81,7 +127,7 @@ func (db *Database) RegisterDialect(di dialectinterface.Interface) {
db.db.Initialize()
}
func (db *Database) SavePaste(p *pastesmodel.Paste) (int64, error) {
func (db *Database) SavePaste(p *structs.Paste) (int64, error) {
return db.db.SavePaste(p)
}

View File

@ -26,8 +26,8 @@ package flatfiles
import (
// local
"github.com/pztrn/fastpastebin/context"
"github.com/pztrn/fastpastebin/database/dialects/interface"
"go.dev.pztrn.name/fastpastebin/internal/context"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
)
var (
@ -38,5 +38,6 @@ var (
func New(cc *context.Context) {
c = cc
f = &FlatFiles{}
c.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
}

View File

@ -37,89 +37,123 @@ import (
"sync"
// local
"github.com/pztrn/fastpastebin/pastes/model"
"go.dev.pztrn.name/fastpastebin/internal/structs"
)
type FlatFiles struct {
pastesIndex []*Index
path string
pastesIndex []Index
writeMutex sync.Mutex
}
// 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 {
c.Logger.Error().Err(err).Msg("Failed to delete paste!")
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) (*pastesmodel.Paste, error) {
func (ff *FlatFiles) GetPaste(pasteID int) (*structs.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)
c.Logger.Debug().Str("path", pastePath).Msg("Trying to load paste data")
pasteInBytes, err := ioutil.ReadFile(pastePath)
if err != nil {
c.Logger.Debug().Msgf("Failed to read paste from storage: %s", err.Error())
c.Logger.Debug().Err(err).Msg("Failed to read paste from storage")
return nil, err
}
c.Logger.Debug().Msgf("Loaded %d bytes: %s", len(pasteInBytes), string(pasteInBytes))
c.Logger.Debug().Int("paste bytes", len(pasteInBytes)).Msg("Loaded paste")
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
paste := &structs.Paste{}
err1 := json.Unmarshal(pasteInBytes, paste)
if err1 != nil {
c.Logger.Error().Err(err1).Msgf("Failed to parse paste")
return nil, err1
}
return paste, nil
}
func (ff *FlatFiles) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
func (ff *FlatFiles) GetPagedPastes(page int) ([]structs.Paste, error) {
// Pagination.
var startPagination = 0
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
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
pastesData := make([]structs.Paste, 0)
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)
c.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)*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)
c.Logger.Debug().Int("paste index", idx).Msg("Paste isn't in pagination query: too high index")
break
}
c.Logger.Debug().Msgf("Getting paste data (ID: %d, index: %d)", paste.ID, idx)
c.Logger.Debug().Int("ID", paste.ID).Int("index", idx).Msg("Getting paste data")
// Get paste data.
pasteData := &pastesmodel.Paste{}
pasteData := &structs.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())
c.Logger.Error().Err(err).Msg("Failed to read paste data")
continue
}
err = json.Unmarshal(pasteRawData, pasteData)
if err != nil {
c.Logger.Error().Msgf("Failed to parse paste data: %s", err.Error())
err1 := json.Unmarshal(pasteRawData, pasteData)
if err1 != nil {
c.Logger.Error().Err(err1).Msg("Failed to parse paste data")
continue
}
@ -131,7 +165,7 @@ func (ff *FlatFiles) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
func (ff *FlatFiles) GetPastesPages() int {
// Get public pastes count.
var publicPastes []*Index
var publicPastes []Index
ff.writeMutex.Lock()
for _, paste := range ff.pastesIndex {
@ -160,33 +194,36 @@ func (ff *FlatFiles) Initialize() {
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.
// Create directory if necessary.
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)
c.Logger.Debug().Str("directory", ff.path).Msg("Directory does not exist, creating...")
_ = os.MkdirAll(ff.path, os.ModePerm)
} else {
c.Logger.Debug().Msgf("Directory '%s' already exists", ff.path)
c.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 {
c.Logger.Debug().Msgf("Directory '%s' does not exist, creating...", filepath.Join(ff.path, "pastes"))
os.MkdirAll(filepath.Join(ff.path, "pastes"), os.ModePerm)
c.Logger.Debug().Str("directory", ff.path).Msg("Directory does not exist, creating...")
_ = os.MkdirAll(filepath.Join(ff.path, "pastes"), os.ModePerm)
} else {
c.Logger.Debug().Msgf("Directory '%s' already exists", filepath.Join(ff.path, "pastes"))
c.Logger.Debug().Str("directory", ff.path).Msg("Directory already exists")
}
// Load pastes index.
ff.pastesIndex = []*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 {
@ -195,52 +232,58 @@ func (ff *FlatFiles) Initialize() {
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())
err1 := json.Unmarshal(indexData, &ff.pastesIndex)
if err1 != nil {
c.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.")
}
c.Logger.Debug().Msgf("Parsed pastes index: %+v", ff.pastesIndex)
c.Logger.Debug().Int("pastes count", len(ff.pastesIndex)).Msg("Parsed pastes index")
}
}
func (ff *FlatFiles) SavePaste(p *pastesmodel.Paste) (int64, error) {
func (ff *FlatFiles) SavePaste(p *structs.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
c.Logger.Debug().Int("new paste ID", pasteID).Msg("Writing paste to disk")
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 := 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())
c.Logger.Error().Err(err).Msg("Failed to encode index data into JSON")
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.")
err1 := ioutil.WriteFile(filepath.Join(ff.path, "pastes", "index.json"), indexData, 0644)
if err1 != nil {
c.Logger.Error().Err(err1).Msg("Failed to write index data to file. Pretty sure that you've lost your pastes.")
return
}
}

View File

@ -29,20 +29,24 @@ import (
"database/sql"
// local
"github.com/pztrn/fastpastebin/pastes/model"
"go.dev.pztrn.name/fastpastebin/internal/structs"
)
type Handler struct{}
func (dbh Handler) DeletePaste(pasteID int) error {
return f.DeletePaste(pasteID)
}
func (dbh Handler) GetDatabaseConnection() *sql.DB {
return f.GetDatabaseConnection()
}
func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
return f.GetPaste(pasteID)
}
func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
return f.GetPagedPastes(page)
}
@ -54,7 +58,7 @@ func (dbh Handler) Initialize() {
f.Initialize()
}
func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) {
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
return f.SavePaste(p)
}

View File

@ -29,15 +29,16 @@ import (
"database/sql"
// local
"github.com/pztrn/fastpastebin/pastes/model"
"go.dev.pztrn.name/fastpastebin/internal/structs"
)
type Interface interface {
DeletePaste(int) error
GetDatabaseConnection() *sql.DB
GetPaste(pasteID int) (*pastesmodel.Paste, error)
GetPagedPastes(page int) ([]pastesmodel.Paste, error)
GetPaste(pasteID int) (*structs.Paste, error)
GetPagedPastes(page int) ([]structs.Paste, error)
GetPastesPages() int
Initialize()
SavePaste(p *pastesmodel.Paste) (int64, error)
SavePaste(p *structs.Paste) (int64, error)
Shutdown()
}

View File

@ -26,8 +26,8 @@ package mysql
import (
// local
"github.com/pztrn/fastpastebin/context"
"github.com/pztrn/fastpastebin/database/dialects/interface"
"go.dev.pztrn.name/fastpastebin/internal/context"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
)
var (
@ -38,5 +38,6 @@ var (
func New(cc *context.Context) {
c = cc
d = &Database{}
c.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
}

View File

@ -29,20 +29,24 @@ import (
"database/sql"
// local
"github.com/pztrn/fastpastebin/pastes/model"
"go.dev.pztrn.name/fastpastebin/internal/structs"
)
type Handler struct{}
func (dbh Handler) DeletePaste(pasteID int) error {
return d.DeletePaste(pasteID)
}
func (dbh Handler) GetDatabaseConnection() *sql.DB {
return d.GetDatabaseConnection()
}
func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
return d.GetPaste(pasteID)
}
func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
return d.GetPagedPastes(page)
}
@ -54,7 +58,7 @@ func (dbh Handler) Initialize() {
d.Initialize()
}
func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) {
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
return d.SavePaste(p)
}

View File

@ -30,7 +30,14 @@ import (
)
func InitialUp(tx *sql.Tx) error {
_, err := tx.Exec("CREATE TABLE `pastes` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Paste ID', `title` text NOT NULL COMMENT 'Paste title', `data` longtext NOT NULL COMMENT 'Paste data', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Paste creation timestamp', `keep_for` int(4) NOT NULL DEFAULT 1 COMMENT 'Keep for integer. 0 - forever.', `keep_for_unit_type` int(1) NOT NULL DEFAULT 1 COMMENT 'Keep for unit type. 1 - minutes, 2 - hours, 3 - days, 4 - months.', PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Pastes';")
_, err := tx.Exec(`CREATE TABLE pastes (
id int(11) NOT NULL AUTO_INCREMENT COMMENT 'Paste ID',
title text NOT NULL COMMENT 'Paste title',
data longtext NOT NULL COMMENT 'Paste data',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Paste creation timestamp',
keep_for int(4) NOT NULL DEFAULT 1 COMMENT 'Keep for integer. 0 - forever.',
keep_for_unit_type int(1) NOT NULL DEFAULT 1 COMMENT 'Keep for unit type. 1 - minutes, 2 - hours, 3 - days, 4 - months.',
PRIMARY KEY (id), UNIQUE KEY id (id)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Pastes';`)
if err != nil {
return err
}

View File

@ -26,10 +26,10 @@ package migrations
import (
// local
"github.com/pztrn/fastpastebin/context"
"go.dev.pztrn.name/fastpastebin/internal/context"
// other
//"github.com/jmoiron/sqlx"
//"gitlab.com/jmoiron/sqlx"
"github.com/pressly/goose"
)
@ -46,7 +46,7 @@ func New(cc *context.Context) {
func Migrate() {
c.Logger.Info().Msg("Migrating database...")
goose.SetDialect("mysql")
_ = goose.SetDialect("mysql")
goose.AddNamedMigration("1_initial.go", InitialUp, nil)
goose.AddNamedMigration("2_paste_lang.go", PasteLangUp, PasteLangDown)
goose.AddNamedMigration("3_private_pastes.go", PrivatePastesUp, PrivatePastesDown)

View File

@ -30,7 +30,8 @@ import (
"fmt"
// local
"github.com/pztrn/fastpastebin/pastes/model"
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/mysql/migrations"
"go.dev.pztrn.name/fastpastebin/internal/structs"
// other
_ "github.com/go-sql-driver/mysql"
@ -42,13 +43,47 @@ 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 {
return err
}
return nil
}
func (db *Database) GetDatabaseConnection() *sql.DB {
return db.db.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) (*pastesmodel.Paste, error) {
p := &pastesmodel.Paste{}
func (db *Database) GetPaste(pasteID int) (*structs.Paste, error) {
db.check()
p := &structs.Paste{}
err := db.db.Get(p, db.db.Rebind("SELECT * FROM `pastes` WHERE id=?"), pasteID)
if err != nil {
return nil, err
@ -57,12 +92,16 @@ func (db *Database) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
return p, nil
}
func (db *Database) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
var pastesRaw []pastesmodel.Paste
var pastes []pastesmodel.Paste
func (db *Database) GetPagedPastes(page int) ([]structs.Paste, error) {
db.check()
var (
pastesRaw []structs.Paste
pastes []structs.Paste
)
// Pagination.
var startPagination = 0
startPagination := 0
if page > 1 {
startPagination = (page - 1) * c.Config.Pastes.Pagination
}
@ -82,8 +121,13 @@ func (db *Database) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
}
func (db *Database) GetPastesPages() int {
var pastesRaw []pastesmodel.Paste
var pastes []pastesmodel.Paste
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
@ -112,7 +156,7 @@ func (db *Database) Initialize() {
// There might be only user, without password. MySQL/MariaDB driver
// in DSN wants "user" or "user:password", "user:" is invalid.
var userpass = ""
var userpass string
if c.Config.Database.Password == "" {
userpass = c.Config.Database.Username
} else {
@ -120,21 +164,29 @@ func (db *Database) Initialize() {
}
dbConnString := fmt.Sprintf("%s@tcp(%s:%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci&charset=utf8mb4", userpass, c.Config.Database.Address, c.Config.Database.Port, c.Config.Database.Database)
c.Logger.Debug().Msgf("Database connection string: %s", dbConnString)
c.Logger.Debug().Str("DSN", dbConnString).Msgf("Database connection string composed")
dbConn, err := sqlx.Connect("mysql", dbConnString)
if err != nil {
c.Logger.Panic().Msgf("Failed to connect to database: %s", err.Error())
c.Logger.Error().Err(err).Msg("Failed to connect to database")
return
}
// Force UTC for current connection.
_ = dbConn.MustExec("SET @@session.time_zone='+00:00';")
c.Logger.Info().Msg("Database connection established")
db.db = dbConn
// Perform migrations.
migrations.New(c)
migrations.Migrate()
}
func (db *Database) SavePaste(p *pastesmodel.Paste) (int64, error) {
func (db *Database) SavePaste(p *structs.Paste) (int64, error) {
db.check()
result, err := db.db.NamedExec("INSERT INTO `pastes` (title, data, created_at, keep_for, keep_for_unit_type, language, private, password, password_salt) VALUES (:title, :data, :created_at, :keep_for, :keep_for_unit_type, :language, :private, :password, :password_salt)", p)
if err != nil {
return 0, err
@ -149,8 +201,10 @@ func (db *Database) SavePaste(p *pastesmodel.Paste) (int64, error) {
}
func (db *Database) Shutdown() {
err := db.db.Close()
if err != nil {
c.Logger.Error().Msgf("Failed to close database connection: %s", err.Error())
if db.db != nil {
err := db.db.Close()
if err != nil {
c.Logger.Error().Err(err).Msg("Failed to close database connection")
}
}
}

View File

@ -0,0 +1,43 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package postgresql
import (
// local
"go.dev.pztrn.name/fastpastebin/internal/context"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
)
var (
c *context.Context
d *Database
)
func New(cc *context.Context) {
c = cc
d = &Database{}
c.Database.RegisterDialect(dialectinterface.Interface(Handler{}))
}

View File

@ -0,0 +1,67 @@
// 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 (
// stdlib
"database/sql"
// local
"go.dev.pztrn.name/fastpastebin/internal/structs"
)
type Handler struct{}
func (dbh Handler) DeletePaste(pasteID int) error {
return d.DeletePaste(pasteID)
}
func (dbh Handler) GetDatabaseConnection() *sql.DB {
return d.GetDatabaseConnection()
}
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
return d.GetPaste(pasteID)
}
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
return d.GetPagedPastes(page)
}
func (dbh Handler) GetPastesPages() int {
return d.GetPastesPages()
}
func (dbh Handler) Initialize() {
d.Initialize()
}
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
return d.SavePaste(p)
}
func (dbh Handler) Shutdown() {
d.Shutdown()
}

View File

@ -0,0 +1,56 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package migrations
import (
// stdlib
"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 {
return err
}
return nil
}

View File

@ -0,0 +1,48 @@
// 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 (
// stdlib
"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 {
return err
}
return nil
}
func PasteLangDown(tx *sql.Tx) error {
_, err := tx.Exec("ALTER TABLE pastes DROP COLUMN language")
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,48 @@
// 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 (
// stdlib
"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 {
return err
}
return nil
}
func PrivatePastesDown(tx *sql.Tx) error {
_, err := tx.Exec("ALTER TABLE pastes DROP COLUMN private")
if err != nil {
return err
}
return nil
}

View File

@ -22,32 +22,37 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package api
package migrations
import (
// other
"github.com/labstack/echo"
// stdlib
"database/sql"
)
// Logs Echo requests.
func echoReqLog(ec echo.Context, next echo.HandlerFunc) error {
c.Logger.Info().
Str("IP", ec.RealIP()).
Str("Host", ec.Request().Host).
Str("Method", ec.Request().Method).
Str("Path", ec.Request().URL.Path).
Str("UA", ec.Request().UserAgent()).
Msg("HTTP request")
func PasswordedPastesUp(tx *sql.Tx) error {
_, err := tx.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 {
return err
}
_, err1 := tx.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 {
return err1
}
next(ec)
return nil
}
// Wrapper around previous function.
func echoReqLogger() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return echoReqLog(c, next)
}
func PasswordedPastesDown(tx *sql.Tx) error {
_, err := tx.Exec("ALTER TABLE pastes DROP COLUMN password")
if err != nil {
return err
}
_, err1 := tx.Exec("ALTER TABLE pastes DROP COLUMN password_salt")
if err1 != nil {
return err1
}
return nil
}

View File

@ -22,49 +22,45 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package api
package migrations
import (
// local
"github.com/pztrn/fastpastebin/api/http"
"github.com/pztrn/fastpastebin/api/json"
"github.com/pztrn/fastpastebin/context"
"go.dev.pztrn.name/fastpastebin/internal/context"
// other
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
//"gitlab.com/jmoiron/sqlx"
"github.com/pressly/goose"
)
var (
c *context.Context
e *echo.Echo
)
// New initializes variables for api package.
// New initializes migrations.
func New(cc *context.Context) {
c = cc
}
// InitializeAPI initializes HTTP API and starts web server.
func InitializeAPI() {
c.Logger.Info().Msg("Initializing HTTP server...")
// Migrate launching migrations.
func Migrate() {
c.Logger.Info().Msg("Migrating database...")
e = echo.New()
e.Use(echoReqLogger())
e.Use(middleware.Recover())
e.DisableHTTP2 = true
e.HideBanner = true
e.HidePort = true
_ = goose.SetDialect("postgres")
goose.AddNamedMigration("1_initial.go", InitialUp, nil)
goose.AddNamedMigration("2_paste_lang.go", PasteLangUp, PasteLangDown)
goose.AddNamedMigration("3_private_pastes.go", PrivatePastesUp, PrivatePastesDown)
goose.AddNamedMigration("4_passworded_pastes.go", PasswordedPastesUp, PasswordedPastesDown)
// Add new migrations BEFORE this message.
c.RegisterEcho(e)
http.New(c)
json.New(c)
listenAddress := c.Config.HTTP.Address + ":" + c.Config.HTTP.Port
go func() {
e.Logger.Fatal(e.Start(listenAddress))
}()
c.Logger.Info().Msgf("Started HTTP server at %s", listenAddress)
dbConn := c.Database.GetDatabaseConnection()
if dbConn != nil {
err := goose.Up(dbConn, ".")
if err != nil {
c.Logger.Info().Msgf("%+v", err)
c.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error())
}
} else {
c.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping")
}
}

View File

@ -0,0 +1,222 @@
// 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 (
// stdlib
"database/sql"
"fmt"
"time"
// local
"go.dev.pztrn.name/fastpastebin/internal/database/dialects/postgresql/migrations"
"go.dev.pztrn.name/fastpastebin/internal/structs"
// other
"github.com/jmoiron/sqlx"
// postgresql adapter
_ "github.com/lib/pq"
)
// 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 {
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()
p := &structs.Paste{}
err := db.db.Get(p, db.db.Rebind("SELECT * FROM pastes WHERE id=$1"), pasteID)
if err != nil {
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 := p.CreatedAt.In(loc)
p.CreatedAt = &utcCreatedAt
return p, 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) * c.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"), c.Config.Pastes.Pagination, startPagination)
if err != nil {
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) / c.Config.Pastes.Pagination
// Check if we have any remainder. Add 1 to pages count if so.
if len(pastes)%c.Config.Pastes.Pagination > 0 {
pages++
}
return pages
}
// Initialize initializes MySQL/MariaDB connection.
func (db *Database) Initialize() {
c.Logger.Info().Msg("Initializing database connection...")
var userpass string
if c.Config.Database.Password == "" {
userpass = c.Config.Database.Username
} else {
userpass = c.Config.Database.Username + ":" + c.Config.Database.Password
}
dbConnString := fmt.Sprintf("postgres://%s@%s:%s/%s?connect_timeout=10&fallback_application_name=fastpastebin&sslmode=disable", userpass, c.Config.Database.Address, c.Config.Database.Port, c.Config.Database.Database)
c.Logger.Debug().Str("DSN", dbConnString).Msg("Database connection string composed")
dbConn, err := sqlx.Connect("postgres", dbConnString)
if err != nil {
c.Logger.Error().Err(err).Msg("Failed to connect to database")
return
}
c.Logger.Info().Msg("Database connection established")
db.db = dbConn
// Perform migrations.
migrations.New(c)
migrations.Migrate()
}
func (db *Database) SavePaste(p *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 {
return 0, err
}
var id int64
err = stmt.Get(&id, p)
if err != nil {
return 0, err
}
return id, nil
}
func (db *Database) Shutdown() {
if db.db != nil {
err := db.db.Close()
if err != nil {
c.Logger.Error().Err(err).Msg("Failed to close database connection")
}
}
}

View File

@ -26,8 +26,8 @@ package database
import (
// local
"github.com/pztrn/fastpastebin/context"
"github.com/pztrn/fastpastebin/database/interface"
"go.dev.pztrn.name/fastpastebin/internal/context"
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
)
var (
@ -39,5 +39,6 @@ var (
func New(cc *context.Context) {
c = cc
d = &Database{}
c.RegisterDatabaseInterface(databaseinterface.Interface(Handler{}))
}

View File

@ -29,23 +29,28 @@ import (
"database/sql"
// local
"github.com/pztrn/fastpastebin/database/dialects/interface"
"github.com/pztrn/fastpastebin/pastes/model"
dialectinterface "go.dev.pztrn.name/fastpastebin/internal/database/dialects/interface"
"go.dev.pztrn.name/fastpastebin/internal/structs"
)
// Handler is an interfaceable structure that proxifies calls from anyone
// to Database structure.
type Handler struct{}
func (dbh Handler) DeletePaste(pasteID int) error {
return d.DeletePaste(pasteID)
}
func (dbh Handler) GetDatabaseConnection() *sql.DB {
return d.GetDatabaseConnection()
}
func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
func (dbh Handler) GetPaste(pasteID int) (*structs.Paste, error) {
return d.GetPaste(pasteID)
}
func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
func (dbh Handler) GetPagedPastes(page int) ([]structs.Paste, error) {
return d.GetPagedPastes(page)
}
@ -62,7 +67,7 @@ func (dbh Handler) RegisterDialect(di dialectinterface.Interface) {
d.RegisterDialect(di)
}
func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) {
func (dbh Handler) SavePaste(p *structs.Paste) (int64, error) {
return d.SavePaste(p)
}

View File

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

View File

@ -6,7 +6,7 @@ import (
"strings"
// local
"github.com/pztrn/fastpastebin/api/http/static"
"go.dev.pztrn.name/fastpastebin/assets/static"
)
// CreateHTML creates pagination HTML based on passed parameters.
@ -33,17 +33,20 @@ func CreateHTML(currentPage int, pages int, linksBase string) string {
}
// First page should always be visible.
var paginationString = ""
var paginationString string
if currentPage == 1 {
paginationString = strings.Replace(string(paginationLinkCurrentRaw), "{pageNum}", strconv.Itoa(currentPage), -1)
} else {
paginationString = strings.Replace(string(paginationLinkRaw), "{pageNum}", "1", -1)
paginationString = strings.Replace(string(paginationString), "{paginationLink}", linksBase+"1", -1)
paginationString = strings.Replace(paginationString, "{paginationLink}", linksBase+"1", -1)
}
var ellipsisStartAdded = false
var ellipsisEndAdded = false
i := 2
var (
ellipsisStartAdded = false
ellipsisEndAdded = false
i = 2
)
for i <= pages {
if pages > 5 {
if currentPage-3 < i && currentPage+3 > i || i == pages {
@ -51,6 +54,7 @@ func CreateHTML(currentPage int, pages int, linksBase string) string {
if i == currentPage {
paginationItemRaw = string(paginationLinkCurrentRaw)
}
paginationItem := strings.Replace(paginationItemRaw, "{pageNum}", strconv.Itoa(i), -1)
paginationItem = strings.Replace(paginationItem, "{paginationLink}", linksBase+strconv.Itoa(i), 1)
paginationString += paginationItem
@ -73,7 +77,7 @@ func CreateHTML(currentPage int, pages int, linksBase string) string {
paginationString += paginationItem
}
i += 1
i++
}
pagination := strings.Replace(string(paginationHTMLRaw), "{paginationLinks}", paginationString, 1)

View File

@ -22,7 +22,7 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package pastesmodel
package structs
import (
// stdlib
@ -36,35 +36,35 @@ import (
)
const (
PASTE_KEEP_FOR_MINUTES = 1
PASTE_KEEP_FOR_HOURS = 2
PASTE_KEEP_FOR_DAYS = 3
PASTE_KEEP_FOR_MONTHS = 4
PasteKeepForever = 0
PasteKeepForMinutes = 1
PasteKeepForHours = 2
PasteKeepForDays = 3
PasteKeepForMonths = 4
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
var (
PASTE_KEEPS_CORELLATION = map[string]int{
"M": PASTE_KEEP_FOR_MINUTES,
"h": PASTE_KEEP_FOR_HOURS,
"d": PASTE_KEEP_FOR_DAYS,
"m": PASTE_KEEP_FOR_MONTHS,
}
)
var PasteKeepsCorrelation = map[string]int{
"M": PasteKeepForMinutes,
"h": PasteKeepForHours,
"d": PasteKeepForDays,
"m": PasteKeepForMonths,
"forever": PasteKeepForever,
}
// Paste represents paste itself.
type Paste struct {
ID int `db:"id" json:"id"`
CreatedAt *time.Time `db:"created_at" json:"created_at"`
Title string `db:"title" json:"title"`
Data string `db:"data" json:"data"`
CreatedAt *time.Time `db:"created_at" json:"created_at"`
KeepFor int `db:"keep_for" json:"keep_for"`
KeepForUnitType int `db:"keep_for_unit_type" json:"keep_for_unit_type"`
Language string `db:"language" json:"language"`
Private bool `db:"private" json:"private"`
Password string `db:"password" json:"password"`
PasswordSalt string `db:"password_salt" json:"password_salt"`
ID int `db:"id" json:"id"`
KeepFor int `db:"keep_for" json:"keep_for"`
KeepForUnitType int `db:"keep_for_unit_type" json:"keep_for_unit_type"`
Private bool `db:"private" json:"private"`
}
// CreatePassword creates password for current paste.
@ -72,6 +72,7 @@ func (p *Paste) CreatePassword(password string) error {
// Create salt - random string.
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
saltBytes := make([]byte, 64)
for i := range saltBytes {
saltBytes[i] = charset[seededRand.Intn(len(charset))]
}
@ -84,6 +85,7 @@ func (p *Paste) CreatePassword(password string) error {
if err != nil {
return err
}
passwordHashBytes := sha256.Sum256(passwordCrypted)
p.Password = fmt.Sprintf("%x", passwordHashBytes)
@ -98,14 +100,17 @@ func (p *Paste) GenerateCryptedCookieValue() string {
func (p *Paste) GetExpirationTime() time.Time {
var expirationTime time.Time
switch p.KeepForUnitType {
case PASTE_KEEP_FOR_MINUTES:
case PasteKeepForever:
expirationTime = time.Now().UTC().Add(time.Hour * 1)
case PasteKeepForMinutes:
expirationTime = p.CreatedAt.Add(time.Minute * time.Duration(p.KeepFor))
case PASTE_KEEP_FOR_HOURS:
case PasteKeepForHours:
expirationTime = p.CreatedAt.Add(time.Hour * time.Duration(p.KeepFor))
case PASTE_KEEP_FOR_DAYS:
case PasteKeepForDays:
expirationTime = p.CreatedAt.Add(time.Hour * 24 * time.Duration(p.KeepFor))
case PASTE_KEEP_FOR_MONTHS:
case PasteKeepForMonths:
expirationTime = p.CreatedAt.Add(time.Hour * 24 * 30 * time.Duration(p.KeepFor))
}
@ -117,11 +122,7 @@ func (p *Paste) IsExpired() bool {
curTime := time.Now().UTC()
expirationTime := p.GetExpirationTime()
if curTime.Sub(expirationTime).Seconds() > 0 {
return true
}
return false
return curTime.Sub(expirationTime).Seconds() > 0
}
// VerifyPassword verifies that provided password is valid.
@ -131,12 +132,9 @@ func (p *Paste) VerifyPassword(password string) bool {
if err != nil {
return false
}
passwordHashBytes := sha256.Sum256(passwordCrypted)
providedPassword := fmt.Sprintf("%x", passwordHashBytes)
if providedPassword == p.Password {
return true
}
return false
return providedPassword == p.Password
}

View File

@ -30,15 +30,17 @@ import (
"strings"
// local
"github.com/pztrn/fastpastebin/api/http/static"
"github.com/pztrn/fastpastebin/context"
"go.dev.pztrn.name/fastpastebin/assets/static"
"go.dev.pztrn.name/fastpastebin/internal/context"
// other
"github.com/labstack/echo"
"github.com/rs/zerolog"
)
var (
c *context.Context
c *context.Context
log zerolog.Logger
)
// GetErrorTemplate returns formatted error template.
@ -56,7 +58,7 @@ func GetRawTemplate(ec echo.Context, templateName string, data map[string]string
// Getting main template.
tplRaw, err := static.ReadFile(templateName)
if err != nil {
ec.String(http.StatusBadRequest, templateName+" not found.")
_ = ec.String(http.StatusBadRequest, templateName+" not found.")
return ""
}
@ -71,26 +73,26 @@ func GetRawTemplate(ec echo.Context, templateName string, data map[string]string
// GetTemplate returns formatted template that can be outputted to client.
func GetTemplate(ec echo.Context, name string, data map[string]string) string {
c.Logger.Debug().Msgf("Requested template '%s'", name)
log.Debug().Str("name", name).Msg("Requested template")
// Getting main template.
mainhtml, err := static.ReadFile("main.html")
if err != nil {
ec.String(http.StatusBadRequest, "main.html not found.")
_ = ec.String(http.StatusBadRequest, "main.html not found.")
return ""
}
// Getting navigation.
navhtml, err1 := static.ReadFile("navigation.html")
if err1 != nil {
ec.String(http.StatusBadRequest, "navigation.html not found.")
_ = ec.String(http.StatusBadRequest, "navigation.html not found.")
return ""
}
// Getting footer.
footerhtml, err2 := static.ReadFile("footer.html")
if err2 != nil {
ec.String(http.StatusBadRequest, "footer.html not found.")
_ = ec.String(http.StatusBadRequest, "footer.html not found.")
return ""
}
@ -103,7 +105,7 @@ func GetTemplate(ec echo.Context, name string, data map[string]string) string {
// Get requested template.
reqhtml, err3 := static.ReadFile(name)
if err3 != nil {
ec.String(http.StatusBadRequest, name+" not found.")
_ = ec.String(http.StatusBadRequest, name+" not found.")
return ""
}
@ -121,4 +123,5 @@ func GetTemplate(ec echo.Context, name string, data map[string]string) string {
// Initialize initializes package.
func Initialize(cc *context.Context) {
c = cc
log = c.Logger.With().Str("type", "internal").Str("package", "templater").Logger()
}

View File

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

View File

@ -16,3 +16,4 @@
_models/
_examples/

55
vendor/github.com/alecthomas/chroma/.golangci.yml generated vendored Normal file
View File

@ -0,0 +1,55 @@
run:
tests: true
skip-dirs:
- _examples
output:
print-issued-lines: false
linters:
enable-all: true
disable:
- maligned
- megacheck
- lll
- gocyclo
- dupl
- gochecknoglobals
- funlen
- godox
- wsl
- gomnd
- gocognit
linters-settings:
govet:
check-shadowing: true
gocyclo:
min-complexity: 10
dupl:
threshold: 100
goconst:
min-len: 8
min-occurrences: 3
issues:
max-per-linter: 0
max-same: 0
exclude-use-default: false
exclude:
# Captured by errcheck.
- '^(G104|G204):'
# Very commonly not checked.
- 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked'
- 'exported method (.*\.MarshalJSON|.*\.UnmarshalJSON|.*\.EntityURN|.*\.GoString|.*\.Pos) should have comment or be unexported'
- 'composite literal uses unkeyed fields'
- 'declaration of "err" shadows declaration'
- 'should not use dot imports'
- 'Potential file inclusion via variable'
- 'should have comment or be unexported'
- 'comment on exported var .* should be of the form'
- 'at least one file in a package should have a package comment'
- 'string literal contains the Unicode'
- 'methods on the same type should have the same receiver name'
- '_TokenType_name should be _TokenTypeName'
- '`_TokenType_map` should be `_TokenTypeMap`'

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