Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
a0f896dc53 | |||
fa7a79310d | |||
ce962ddd00 | |||
164a37d41f | |||
3ebff29113 | |||
7f04a9a7bb | |||
a2a21482e2 | |||
ffb42f43eb | |||
3f9801b1bd | |||
825fd724ff | |||
5f58741159 | |||
47672c586d | |||
11897d0e1a | |||
02cea49fc4 | |||
a52b18ffe4 | |||
da4bc379d8 | |||
9c9f0c1f68 | |||
6a787e7e23 | |||
cdc8ecf49b | |||
bd981023da | |||
a982e07faf | |||
0bbb415852 | |||
6207229e9b | |||
2159aadfcb | |||
3ebf9b653e | |||
4d60a9b1b6 | |||
e042c4414f | |||
333fc1d12a | |||
7f2174a33e | |||
e26a466efc | |||
|
d9a46aa5b3 | ||
|
dbf82e213b | ||
4f01e2f5ce | |||
7281b9be65 | |||
19b5ef3d9f | |||
3fe51fc6c5 | |||
19a3a5004c | |||
0bf20cd2c9 | |||
35c217fe46 | |||
849c72b238 | |||
c565ec8f21 | |||
aa5e11329f | |||
b6556d6e90 | |||
400ce0db0c | |||
fc8f4e9d8b |
69
.drone.yml
Normal file
69
.drone.yml
Normal 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
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1 +1,6 @@
|
|||
examples/fastpastebin.yaml
|
||||
dist/
|
||||
data/
|
||||
.idea
|
||||
.vscode
|
||||
*DS_Store*
|
||||
|
|
31
.gitlab-ci.yml
Normal file
31
.gitlab-ci.yml
Normal 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
21
.golangci.yml
Normal 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
17
Dockerfile
Normal 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
187
Gopkg.lock
generated
|
@ -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
|
34
Gopkg.toml
34
Gopkg.toml
|
@ -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
|
67
README.md
67
README.md
|
@ -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.
|
||||
* 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
1
assets/css/bulma-0.7.0.min.css
vendored
1
assets/css/bulma-0.7.0.min.css
vendored
File diff suppressed because one or more lines are too long
1
assets/css/bulma-0.7.5.min.css
vendored
Normal file
1
assets/css/bulma-0.7.5.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/css/bulma-tooltip-1.0.4.min.css
vendored
1
assets/css/bulma-tooltip-1.0.4.min.css
vendored
File diff suppressed because one or more lines are too long
1
assets/css/bulma-tooltip-3.0.0.min.css
vendored
Normal file
1
assets/css/bulma-tooltip-3.0.0.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
|
@ -7,3 +7,8 @@
|
|||
.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
|
||||
}
|
6
assets/database_not_available.html
Normal file
6
assets/database_not_available.html
Normal 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>
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,21 +36,21 @@ var (
|
|||
)
|
||||
|
||||
// HTTPFS implements http.FileSystem
|
||||
type HTTPFS struct {}
|
||||
|
||||
|
||||
|
||||
func init() {
|
||||
if CTX.Err() != nil {
|
||||
panic(CTX.Err())
|
||||
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() {
|
||||
err := CTX.Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
|
||||
var err error
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
35
assets/static/b0xfile__database_not_available.html.go
Normal file
35
assets/static/b0xfile__database_not_available.html.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
@ -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() {
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
35
assets/static/b0xfile_static_css_bulma-0.7.5.min.css.go
Normal file
35
assets/static/b0xfile_static_css_bulma-0.7.5.min.css.go
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
35
assets/static/b0xfile_static_css_bulma.css.map.go
Normal file
35
assets/static/b0xfile_static_css_bulma.css.map.go
Normal file
File diff suppressed because one or more lines are too long
|
@ -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() {
|
||||
|
|
@ -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
26
builder.sh
Executable 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
|
|
@ -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
37
docker-compose.yml
Normal 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
|
22
docker/fastpastebin.docker.yaml
Normal file
22
docker/fastpastebin.docker.yaml
Normal 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
10
docker/nginx.conf
Normal 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
7
docker/set_docker_tag.sh
Normal 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
|
|
@ -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.")
|
||||
}
|
43
domains/dbnotavailable/exported.go
Normal file
43
domains/dbnotavailable/exported.go
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
@ -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
337
domains/pastes/paste_get.go
Normal 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)
|
||||
}
|
153
domains/pastes/paste_post.go
Normal file
153
domains/pastes/paste_post.go
Normal 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)
|
||||
}
|
111
domains/pastes/pastes_get.go
Normal file
111
domains/pastes/pastes_get.go
Normal 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)
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
21
go.mod
Normal 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
107
go.sum
Normal 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=
|
|
@ -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
|
||||
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
|
||||
}
|
||||
|
|
@ -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"`
|
|
@ -24,9 +24,10 @@
|
|||
|
||||
package config
|
||||
|
||||
// ConfigHTTP describes HTTP server configuration.
|
||||
type ConfigHTTP struct {
|
||||
// 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"`
|
||||
}
|
|
@ -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"`
|
|
@ -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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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.
|
|
@ -26,7 +26,7 @@ package context
|
|||
|
||||
const (
|
||||
// Version .
|
||||
Version = "0.2.0"
|
||||
Version = "0.4.0"
|
||||
)
|
||||
|
||||
// New creates new context.
|
47
internal/context/http_server.go
Normal file
47
internal/context/http_server.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
80
internal/context/logger.go
Normal file
80
internal/context/logger.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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{}))
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
|
@ -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{}))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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 {
|
||||
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() {
|
||||
if db.db != nil {
|
||||
err := db.db.Close()
|
||||
if err != nil {
|
||||
c.Logger.Error().Msgf("Failed to close database connection: %s", err.Error())
|
||||
c.Logger.Error().Err(err).Msg("Failed to close database connection")
|
||||
}
|
||||
}
|
||||
}
|
43
internal/database/dialects/postgresql/exported.go
Normal file
43
internal/database/dialects/postgresql/exported.go
Normal 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{}))
|
||||
}
|
67
internal/database/dialects/postgresql/handler.go
Normal file
67
internal/database/dialects/postgresql/handler.go
Normal 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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
222
internal/database/dialects/postgresql/postgresqldatabase.go
Normal file
222
internal/database/dialects/postgresql/postgresqldatabase.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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{}))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
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()
|
||||
}
|
|
@ -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))
|
||||
}
|
1
vendor/github.com/alecthomas/chroma/.gitignore
generated
vendored
1
vendor/github.com/alecthomas/chroma/.gitignore
generated
vendored
|
@ -16,3 +16,4 @@
|
|||
|
||||
_models/
|
||||
|
||||
_examples/
|
||||
|
|
55
vendor/github.com/alecthomas/chroma/.golangci.yml
generated
vendored
Normal file
55
vendor/github.com/alecthomas/chroma/.golangci.yml
generated
vendored
Normal 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
Loading…
Reference in New Issue
Block a user