7 Commits

Author SHA1 Message Date
2beb9ecef9 Version bump - we're 0.2.0! 2018-05-27 12:27:43 +05:00
0cca0f453f Database dialects, proper database shutdown, pagination in configuration.
Added possibility to use different database (storage) backends. Currently
supported: flatfiles and mysql. Fixes #2.

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

De-hardcoded pagination count. Fixes #12.
2018-05-27 12:25:01 +05:00
296e2771d6 Version 0.1.1. 2018-05-26 16:10:38 +05:00
c8005a4836 Navbar burger isn't used, removed. 2018-05-26 13:58:50 +05:00
342a4d1d7c Templates and templater, added footer with application data. Fixes #8 and #7. 2018-05-26 13:50:13 +05:00
d7e9865c91 Proper check for paste's password input. Fixes #4 and should also fix #5. 2018-05-26 12:09:53 +05:00
9f0beb7e3e Set proper logger level, fixes #6. 2018-05-26 12:05:37 +05:00
43 changed files with 1386 additions and 628 deletions

View File

@@ -3,9 +3,7 @@
# Fast Pastebin # Fast Pastebin
Easy-to-use-and-install pastebin software written in Go. No bells or Easy-to-use-and-install pastebin software written in Go. No bells or
whistles, no websockets and even NO JAVASCRIPT!(*) whistles, no websockets and even NO JAVASCRIPT!
(*) Except fontawesome, because it's awesome :).
# Current functionality. # Current functionality.
@@ -13,6 +11,7 @@ whistles, no websockets and even NO JAVASCRIPT!(*)
* Syntax highlighting. * Syntax highlighting.
* Pastes expiration. * Pastes expiration.
* Passwords for pastes. * Passwords for pastes.
* Multiple storage backends. Currently: ``flatfiles`` and ``mysql``.
# Caveats. # Caveats.

View File

@@ -27,11 +27,10 @@ package http
import ( import (
// stdlib // stdlib
"net/http" "net/http"
"strings"
// local // local
"github.com/pztrn/fastpastebin/api/http/static"
"github.com/pztrn/fastpastebin/captcha" "github.com/pztrn/fastpastebin/captcha"
"github.com/pztrn/fastpastebin/templater"
// other // other
"github.com/alecthomas/chroma/lexers" "github.com/alecthomas/chroma/lexers"
@@ -40,11 +39,6 @@ import (
// Index of this site. // Index of this site.
func indexGet(ec echo.Context) error { func indexGet(ec echo.Context) error {
htmlRaw, err := static.ReadFile("index.html")
if err != nil {
return ec.String(http.StatusNotFound, "index.html wasn't found!")
}
// Generate list of available languages to highlight. // Generate list of available languages to highlight.
availableLexers := lexers.Names(false) availableLexers := lexers.Names(false)
@@ -53,11 +47,10 @@ func indexGet(ec echo.Context) error {
availableLexersSelectOpts += "<option value='" + availableLexers[i] + "'>" + availableLexers[i] + "</option>" availableLexersSelectOpts += "<option value='" + availableLexers[i] + "'>" + availableLexers[i] + "</option>"
} }
html := strings.Replace(string(htmlRaw), "{lexers}", availableLexersSelectOpts, 1)
// Captcha. // Captcha.
captchaString := captcha.NewCaptcha() captchaString := captcha.NewCaptcha()
html = strings.Replace(html, "{captchaString}", captchaString, -1)
return ec.HTML(http.StatusOK, html) htmlData := templater.GetTemplate(ec, "index.html", map[string]string{"lexers": availableLexersSelectOpts, "captchaString": captchaString})
return ec.HTML(http.StatusOK, htmlData)
} }

View File

@@ -1,5 +1,5 @@
// Code generated by fileb0x at "2018-05-18 22:25:13.180720675 +0500 +05 m=+0.030971779" from config file "fileb0x.yml" DO NOT EDIT. // 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(1679c5529785a2e624071b2679319ecb.608888160ddb195ed89fce5ab290ff92) // modification hash(e9e210106a4d77680942064c40d06a3e.522a9cc525fa984df98098d4c3992752)
package static package static

View File

@@ -1,5 +1,5 @@
// Code generaTed by fileb0x at "2018-05-01 18:35:23.702577304 +0500 +05 m=+0.047872409" from config file "fileb0x.yml" DO NOT EDIT. // 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-01 18:35:02 +0500 +05) // modified(2018-05-26 13:29:04.002355958 +0500 +05)
// original path: assets/error.html // original path: assets/error.html
package static package static
@@ -10,7 +10,7 @@ import (
) )
// FileErrorHTML is "/error.html" // FileErrorHTML is "/error.html"
var FileErrorHTML = []byte("\x3c\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x74\x6d\x6c\x3e\x0a\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\x66\x2d\x38\x22\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x6e\x61\x6d\x65\x3d\x22\x76\x69\x65\x77\x70\x6f\x72\x74\x22\x20\x63\x6f\x6e\x74\x65\x6e\x74\x3d\x22\x77\x69\x64\x74\x68\x3d\x64\x65\x76\x69\x63\x65\x2d\x77\x69\x64\x74\x68\x2c\x20\x69\x6e\x69\x74\x69\x61\x6c\x2d\x73\x63\x61\x6c\x65\x3d\x31\x22\x3e\x0a\x20\x20\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x20\x2d\x20\x45\x52\x52\x4f\x52\x21\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x62\x75\x6c\x6d\x61\x2d\x30\x2e\x37\x2e\x30\x2e\x6d\x69\x6e\x2e\x63\x73\x73\x22\x3e\x0a\x20\x20\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x64\x65\x66\x65\x72\x20\x73\x72\x63\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x6a\x73\x2f\x66\x6f\x6e\x74\x61\x77\x65\x73\x6f\x6d\x65\x2d\x35\x2e\x30\x2e\x37\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x73\x74\x79\x6c\x65\x2e\x63\x73\x73\x22\x3e\x0a\x3c\x2f\x68\x65\x61\x64\x3e\x0a\x0a\x3c\x62\x6f\x64\x79\x3e\x0a\x20\x20\x20\x20\x3c\x6e\x61\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x20\x69\x73\x2d\x64\x61\x72\x6b\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x62\x72\x61\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x22\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x70\x61\x73\x74\x65\x73\x2f\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x50\x61\x73\x74\x65\x73\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x62\x75\x72\x67\x65\x72\x20\x62\x75\x72\x67\x65\x72\x22\x20\x64\x61\x74\x61\x2d\x74\x61\x72\x67\x65\x74\x3d\x22\x6e\x61\x76\x62\x61\x72\x49\x74\x65\x6d\x73\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x69\x64\x3d\x22\x6e\x61\x76\x62\x61\x72\x49\x74\x65\x6d\x73\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x6d\x65\x6e\x75\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x73\x74\x61\x72\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x65\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x6e\x61\x76\x3e\x0a\x20\x20\x20\x20\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x3e\x7b\x65\x72\x72\x6f\x72\x7d\x3c\x2f\x70\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e\x0a\x3c\x2f\x62\x6f\x64\x79\x3e\x0a\x0a\x3c\x2f\x68\x74\x6d\x6c\x3e") var FileErrorHTML = []byte("\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x62\x6f\x78\x20\x68\x61\x73\x2d\x62\x61\x63\x6b\x67\x72\x6f\x75\x6e\x64\x2d\x77\x61\x72\x6e\x69\x6e\x67\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x3e\x7b\x65\x72\x72\x6f\x72\x7d\x3c\x2f\x70\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e")
func init() { func init() {

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
// Code generaTed by fileb0x at "2018-05-18 22:25:13.191256649 +0500 +05 m=+0.041507879" from config file "fileb0x.yml" DO NOT EDIT. // 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-18 22:17:55.447214662 +0500 +05) // modified(2018-05-26 13:41:01.75233841 +0500 +05)
// original path: assets/pastelist_list.html // original path: assets/pastelist_list.html
package static package static
@@ -10,7 +10,7 @@ import (
) )
// FilePastelistListHTML is "/pastelist_list.html" // FilePastelistListHTML is "/pastelist_list.html"
var FilePastelistListHTML = []byte("\x3c\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x74\x6d\x6c\x3e\x0a\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\x66\x2d\x38\x22\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x6e\x61\x6d\x65\x3d\x22\x76\x69\x65\x77\x70\x6f\x72\x74\x22\x20\x63\x6f\x6e\x74\x65\x6e\x74\x3d\x22\x77\x69\x64\x74\x68\x3d\x64\x65\x76\x69\x63\x65\x2d\x77\x69\x64\x74\x68\x2c\x20\x69\x6e\x69\x74\x69\x61\x6c\x2d\x73\x63\x61\x6c\x65\x3d\x31\x22\x3e\x0a\x20\x20\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x20\x2d\x20\x50\x61\x73\x74\x65\x73\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x62\x75\x6c\x6d\x61\x2d\x30\x2e\x37\x2e\x30\x2e\x6d\x69\x6e\x2e\x63\x73\x73\x22\x3e\x0a\x20\x20\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x64\x65\x66\x65\x72\x20\x73\x72\x63\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x6a\x73\x2f\x66\x6f\x6e\x74\x61\x77\x65\x73\x6f\x6d\x65\x2d\x35\x2e\x30\x2e\x37\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x20\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x73\x74\x61\x74\x69\x63\x2f\x63\x73\x73\x2f\x73\x74\x79\x6c\x65\x2e\x63\x73\x73\x22\x3e\x0a\x3c\x2f\x68\x65\x61\x64\x3e\x0a\x0a\x3c\x62\x6f\x64\x79\x3e\x0a\x20\x20\x20\x20\x3c\x6e\x61\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x20\x69\x73\x2d\x64\x61\x72\x6b\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x62\x72\x61\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x22\x3e\x46\x61\x73\x74\x20\x50\x61\x73\x74\x65\x20\x42\x69\x6e\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x61\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x69\x74\x65\x6d\x22\x20\x68\x72\x65\x66\x3d\x22\x2f\x70\x61\x73\x74\x65\x73\x2f\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x50\x61\x73\x74\x65\x73\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x61\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x62\x75\x72\x67\x65\x72\x20\x62\x75\x72\x67\x65\x72\x22\x20\x64\x61\x74\x61\x2d\x74\x61\x72\x67\x65\x74\x3d\x22\x6e\x61\x76\x62\x61\x72\x49\x74\x65\x6d\x73\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x73\x70\x61\x6e\x3e\x3c\x2f\x73\x70\x61\x6e\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x69\x64\x3d\x22\x6e\x61\x76\x62\x61\x72\x49\x74\x65\x6d\x73\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x6d\x65\x6e\x75\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x73\x74\x61\x72\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x22\x6e\x61\x76\x62\x61\x72\x2d\x65\x6e\x64\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x6e\x61\x76\x3e\x0a\x20\x20\x20\x20\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x7d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x73\x74\x65\x73\x7d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x7d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e\x0a\x3c\x2f\x62\x6f\x64\x79\x3e\x0a\x0a\x3c\x2f\x68\x74\x6d\x6c\x3e") var FilePastelistListHTML = []byte("\x3c\x73\x65\x63\x74\x69\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x73\x65\x63\x74\x69\x6f\x6e\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x73\x74\x65\x73\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x70\x61\x67\x69\x6e\x61\x74\x69\x6f\x6e\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x73\x65\x63\x74\x69\x6f\x6e\x3e")
func init() { func init() {

View File

@@ -1,40 +1,5 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fast Paste Bin - ERROR!</title>
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar is-dark">
<div class="navbar-brand">
<a class="navbar-item" href="/">Fast Paste Bin</a>
<a class="navbar-item" href="/pastes/">
Pastes
</a>
<div class="navbar-burger burger" data-target="navbarItems">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="navbarItems" class="navbar-menu">
<div class="navbar-start">
</div>
<div class="navbar-end">
</div>
</div>
</nav>
<section class="section"> <section class="section">
<div class="box has-background-warning">
<p>{error}</p> <p>{error}</p>
</div>
</section> </section>
</body>
</html>

11
assets/footer.html Normal file
View File

@@ -0,0 +1,11 @@
<div class="container">
<div class="content has-text-centered">
<p>
<strong>Fast paste bin</strong> version
<strong>{version}</strong> by
<a href="https://pztrn.name">Stanislav N. aka pztrn</a>. The source code is licensed
<a href="http://opensource.org/licenses/mit-license.php">MIT</a>. Get
<a href="https://github.com/pztrn/fastpastebin">source or binary releases here</a>!
</p>
</div>
</div>

View File

@@ -1,38 +1,3 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fast Paste Bin</title>
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
<link rel="stylesheet" href="/static/css/bulma-tooltip-1.0.4.min.css">
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar is-dark">
<div class="navbar-brand">
<a class="navbar-item" href="/">Fast Paste Bin</a>
<a class="navbar-item" href="/pastes/">
Pastes
</a>
<div class="navbar-burger burger" data-target="navbarItems">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="navbarItems" class="navbar-menu">
<div class="navbar-start">
</div>
<div class="navbar-end">
</div>
</div>
</nav>
<section class="section"> <section class="section">
<form action="/paste/" method="POST" autocomplete="off"> <form action="/paste/" method="POST" autocomplete="off">
<div class="columns"> <div class="columns">
@@ -115,6 +80,3 @@
</div> </div>
</form> </form>
</section> </section>
</body>
</html>

20
assets/main.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fast Paste Bin</title>
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
<link rel="stylesheet" href="/static/css/bulma-tooltip-1.0.4.min.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
{navigation} {documentBody}
</body>
<footer class="footer">
{footer}
</footer>
</html>

16
assets/navigation.html Normal file
View File

@@ -0,0 +1,16 @@
<nav class="navbar is-dark">
<div class="navbar-brand">
<a class="navbar-item" href="/">Fast Paste Bin</a>
<a class="navbar-item" href="/pastes/">
Pastes
</a>
</div>
<div id="navbarItems" class="navbar-menu">
<div class="navbar-start">
</div>
<div class="navbar-end">
</div>
</div>
</nav>

View File

@@ -1,37 +1,3 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fast Paste Bin - Paste is protected with password</title>
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar is-dark">
<div class="navbar-brand">
<a class="navbar-item" href="/">Fast Paste Bin</a>
<a class="navbar-item" href="/pastes/">
Pastes
</a>
<div class="navbar-burger burger" data-target="navbarItems">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="navbarItems" class="navbar-menu">
<div class="navbar-start">
</div>
<div class="navbar-end">
</div>
</div>
</nav>
<section class="section"> <section class="section">
<div class="content"> <div class="content">
<div class="columns"> <div class="columns">
@@ -64,6 +30,3 @@
</div> </div>
</div> </div>
</section> </section>
</body>
</html>

View File

@@ -1,37 +1,3 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fast Paste Bin - Paste #{pasteID}</title>
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar is-dark">
<div class="navbar-brand">
<a class="navbar-item" href="/">Fast Paste Bin</a>
<a class="navbar-item" href="/pastes/">
Pastes
</a>
<div class="navbar-burger burger" data-target="navbarItems">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="navbarItems" class="navbar-menu">
<div class="navbar-start">
</div>
<div class="navbar-end">
</div>
</div>
</nav>
<section class="section"> <section class="section">
<div class="content"> <div class="content">
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth"> <table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
@@ -66,6 +32,3 @@
<div class="paste-data"> <div class="paste-data">
{pastedata} {pastedata}
</div> </div>
</body>
</html>

View File

@@ -1,37 +1,3 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fast Paste Bin - Pastes</title>
<link rel="stylesheet" href="/static/css/bulma-0.7.0.min.css">
<script defer src="/static/js/fontawesome-5.0.7.js"></script>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar is-dark">
<div class="navbar-brand">
<a class="navbar-item" href="/">Fast Paste Bin</a>
<a class="navbar-item" href="/pastes/">
Pastes
</a>
<div class="navbar-burger burger" data-target="navbarItems">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="navbarItems" class="navbar-menu">
<div class="navbar-start">
</div>
<div class="navbar-end">
</div>
</div>
</nav>
<section class="section"> <section class="section">
<div> <div>
{pagination} {pagination}
@@ -43,6 +9,3 @@
{pagination} {pagination}
</div> </div>
</section> </section>
</body>
</html>

View File

@@ -37,6 +37,7 @@ import (
"github.com/pztrn/fastpastebin/database" "github.com/pztrn/fastpastebin/database"
"github.com/pztrn/fastpastebin/database/migrations" "github.com/pztrn/fastpastebin/database/migrations"
"github.com/pztrn/fastpastebin/pastes" "github.com/pztrn/fastpastebin/pastes"
"github.com/pztrn/fastpastebin/templater"
) )
func main() { func main() {
@@ -57,6 +58,7 @@ func main() {
c.Database.Initialize() c.Database.Initialize()
migrations.New(c) migrations.New(c)
migrations.Migrate() migrations.Migrate()
templater.Initialize(c)
api.New(c) api.New(c)
api.InitializeAPI() api.InitializeAPI()

View File

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

30
config/pastes.go Normal file
View File

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

View File

@@ -29,4 +29,5 @@ type ConfigStruct struct {
Database ConfigDatabase `yaml:"database"` Database ConfigDatabase `yaml:"database"`
Logging ConfigLogging `yaml:"logging"` Logging ConfigLogging `yaml:"logging"`
HTTP ConfigHTTP `yaml:"http"` HTTP ConfigHTTP `yaml:"http"`
Pastes ConfigPastes `yaml:"pastes"`
} }

View File

@@ -114,6 +114,23 @@ func (c *Context) LoadConfiguration() {
// Yay! See what it gets! // Yay! See what it gets!
c.Logger.Debug().Msgf("Parsed configuration: %+v", c.Config) c.Logger.Debug().Msgf("Parsed configuration: %+v", c.Config)
// Set log level.
c.Logger.Info().Msgf("Setting logger level: %s", c.Config.Logging.LogLevel)
switch c.Config.Logging.LogLevel {
case "DEBUG":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "INFO":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "WARN":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "ERROR":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case "FATAL":
zerolog.SetGlobalLevel(zerolog.FatalLevel)
case "PANIC":
zerolog.SetGlobalLevel(zerolog.PanicLevel)
}
} }
// RegisterDatabaseInterface registers database interface for later use. // RegisterDatabaseInterface registers database interface for later use.
@@ -129,4 +146,6 @@ func (c *Context) RegisterEcho(e *echo.Echo) {
// Shutdown shutdowns entire application. // Shutdown shutdowns entire application.
func (c *Context) Shutdown() { func (c *Context) Shutdown() {
c.Logger.Info().Msg("Shutting down Fast Pastebin...") c.Logger.Info().Msg("Shutting down Fast Pastebin...")
c.Database.Shutdown()
} }

View File

@@ -24,6 +24,11 @@
package context package context
const (
// Version .
Version = "0.2.0"
)
// New creates new context. // New creates new context.
func New() *Context { func New() *Context {
return &Context{} return &Context{}

View File

@@ -26,47 +26,65 @@ package database
import ( import (
// stdlib // stdlib
"fmt" "database/sql"
// local
"github.com/pztrn/fastpastebin/database/dialects/flatfiles"
"github.com/pztrn/fastpastebin/database/dialects/interface"
"github.com/pztrn/fastpastebin/database/dialects/mysql"
"github.com/pztrn/fastpastebin/pastes/model"
// other // other
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
) )
// Database represents control structure for database connection. // Database represents control structure for database connection.
type Database struct { type Database struct {
db *sqlx.DB db dialectinterface.Interface
} }
// GetDatabaseConnection returns current database connection. func (db *Database) GetDatabaseConnection() *sql.DB {
func (db *Database) GetDatabaseConnection() *sqlx.DB { if db.db != nil {
return db.db return db.db.GetDatabaseConnection()
}
return nil
}
func (db *Database) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
return db.db.GetPaste(pasteID)
}
func (db *Database) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
return db.db.GetPagedPastes(page)
}
func (db *Database) GetPastesPages() int {
return db.db.GetPastesPages()
} }
// Initialize initializes connection to database. // Initialize initializes connection to database.
func (db *Database) Initialize() { func (db *Database) Initialize() {
c.Logger.Info().Msg("Initializing database connection...") c.Logger.Info().Msg("Initializing database connection...")
// There might be only user, without password. MySQL/MariaDB driver if c.Config.Database.Type == "mysql" {
// in DSN wants "user" or "user:password", "user:" is invalid. mysql.New(c)
var userpass = "" } else if c.Config.Database.Type == "flatfiles" {
if c.Config.Database.Password == "" { flatfiles.New(c)
userpass = c.Config.Database.Username
} else { } else {
userpass = c.Config.Database.Username + ":" + c.Config.Database.Password c.Logger.Fatal().Msgf("Unknown database type: %s", c.Config.Database.Type)
}
} }
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) func (db *Database) RegisterDialect(di dialectinterface.Interface) {
c.Logger.Debug().Msgf("Database connection string: %s", dbConnString) db.db = di
db.db.Initialize()
dbConn, err := sqlx.Connect("mysql", dbConnString)
if err != nil {
c.Logger.Panic().Msgf("Failed to connect to database: %s", err.Error())
} }
// Force UTC for current connection. func (db *Database) SavePaste(p *pastesmodel.Paste) (int64, error) {
_ = dbConn.MustExec("SET @@session.time_zone='+00:00';") return db.db.SavePaste(p)
}
c.Logger.Info().Msg("Database connection established")
db.db = dbConn func (db *Database) Shutdown() {
db.db.Shutdown()
} }

View File

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

View File

@@ -0,0 +1,246 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package flatfiles
import (
// stdlib
"database/sql"
"encoding/json"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"sync"
// local
"github.com/pztrn/fastpastebin/pastes/model"
)
type FlatFiles struct {
pastesIndex []*Index
path string
writeMutex sync.Mutex
}
func (ff *FlatFiles) GetDatabaseConnection() *sql.DB {
return nil
}
func (ff *FlatFiles) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
ff.writeMutex.Lock()
pastePath := filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json")
c.Logger.Debug().Msgf("Trying to load paste data from '%s'...", pastePath)
pasteInBytes, err := ioutil.ReadFile(pastePath)
if err != nil {
c.Logger.Debug().Msgf("Failed to read paste from storage: %s", err.Error())
return nil, err
}
c.Logger.Debug().Msgf("Loaded %d bytes: %s", len(pasteInBytes), string(pasteInBytes))
ff.writeMutex.Unlock()
paste := &pastesmodel.Paste{}
err = json.Unmarshal(pasteInBytes, paste)
if err != nil {
c.Logger.Error().Msgf("Failed to parse paste: %s", err.Error())
return nil, err
}
return paste, nil
}
func (ff *FlatFiles) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
// Pagination.
var startPagination = 0
if page > 1 {
startPagination = (page - 1) * c.Config.Pastes.Pagination
}
c.Logger.Debug().Msgf("Pastes index: %+v", ff.pastesIndex)
// Iteration one - get only public pastes.
var publicPastes []*Index
for _, paste := range ff.pastesIndex {
if !paste.Private {
publicPastes = append(publicPastes, paste)
}
}
c.Logger.Debug().Msgf("%+v", publicPastes)
// Iteration two - get paginated pastes.
var pastesData []pastesmodel.Paste
for idx, paste := range publicPastes {
if len(pastesData) == c.Config.Pastes.Pagination {
break
}
if idx < startPagination {
c.Logger.Debug().Msgf("Paste with index %d isn't in pagination query: too low index", idx)
continue
}
if (idx-1 >= startPagination && page > 1 && idx > startPagination+((page-1)*c.Config.Pastes.Pagination)) || (idx-1 >= startPagination && page == 1 && idx > startPagination+(page*c.Config.Pastes.Pagination)) {
c.Logger.Debug().Msgf("Paste with index %d isn't in pagination query: too high index", idx)
break
}
c.Logger.Debug().Msgf("Getting paste data (ID: %d, index: %d)", paste.ID, idx)
// Get paste data.
pasteData := &pastesmodel.Paste{}
pasteRawData, err := ioutil.ReadFile(filepath.Join(ff.path, "pastes", strconv.Itoa(paste.ID)+".json"))
if err != nil {
c.Logger.Error().Msgf("Failed to read paste data: %s", err.Error())
continue
}
err = json.Unmarshal(pasteRawData, pasteData)
if err != nil {
c.Logger.Error().Msgf("Failed to parse paste data: %s", err.Error())
continue
}
pastesData = append(pastesData, (*pasteData))
}
return pastesData, nil
}
func (ff *FlatFiles) GetPastesPages() int {
// Get public pastes count.
var publicPastes []*Index
ff.writeMutex.Lock()
for _, paste := range ff.pastesIndex {
if !paste.Private {
publicPastes = append(publicPastes, paste)
}
}
ff.writeMutex.Unlock()
// Calculate pages.
pages := len(publicPastes) / c.Config.Pastes.Pagination
// Check if we have any remainder. Add 1 to pages count if so.
if len(publicPastes)%c.Config.Pastes.Pagination > 0 {
pages++
}
return pages
}
func (ff *FlatFiles) Initialize() {
c.Logger.Info().Msg("Initializing flatfiles storage...")
path := c.Config.Database.Path
// Get proper paste file path.
if strings.Contains(c.Config.Database.Path, "~") {
curUser, err := user.Current()
if err != nil {
c.Logger.Error().Msg("Failed to get current user. Will replace '~' for '/' in storage path!")
path = strings.Replace(path, "~", "/", -1)
}
path = strings.Replace(path, "~", curUser.HomeDir, -1)
}
path, _ = filepath.Abs(path)
ff.path = path
c.Logger.Debug().Msgf("Storage path is now: %s", ff.path)
// Create directory if neccessary.
if _, err := os.Stat(ff.path); err != nil {
c.Logger.Debug().Msgf("Directory '%s' does not exist, creating...", ff.path)
os.MkdirAll(ff.path, os.ModePerm)
} else {
c.Logger.Debug().Msgf("Directory '%s' already exists", ff.path)
}
// Create directory for pastes.
if _, err := os.Stat(filepath.Join(ff.path, "pastes")); err != nil {
c.Logger.Debug().Msgf("Directory '%s' does not exist, creating...", filepath.Join(ff.path, "pastes"))
os.MkdirAll(filepath.Join(ff.path, "pastes"), os.ModePerm)
} else {
c.Logger.Debug().Msgf("Directory '%s' already exists", filepath.Join(ff.path, "pastes"))
}
// Load pastes index.
ff.pastesIndex = []*Index{}
if _, err := os.Stat(filepath.Join(ff.path, "pastes", "index.json")); err != nil {
c.Logger.Warn().Msg("Pastes index file does not exist, will create new one")
} else {
indexData, err := ioutil.ReadFile(filepath.Join(ff.path, "pastes", "index.json"))
if err != nil {
c.Logger.Fatal().Msg("Failed to read contents of index file!")
}
err = json.Unmarshal(indexData, &ff.pastesIndex)
if err != nil {
c.Logger.Error().Msgf("Failed to parse index file contents from JSON into internal structure. Will create new index file. All of your previous pastes will became unavailable. Error was: %s", err.Error())
}
c.Logger.Debug().Msgf("Parsed pastes index: %+v", ff.pastesIndex)
}
}
func (ff *FlatFiles) SavePaste(p *pastesmodel.Paste) (int64, error) {
ff.writeMutex.Lock()
// Write paste data on disk.
filesOnDisk, _ := ioutil.ReadDir(filepath.Join(ff.path, "pastes"))
pasteID := len(filesOnDisk) + 1
c.Logger.Debug().Msgf("Writing paste to disk, ID will be " + strconv.Itoa(pasteID))
p.ID = pasteID
data, err := json.Marshal(p)
if err != nil {
ff.writeMutex.Unlock()
return 0, err
}
err = ioutil.WriteFile(filepath.Join(ff.path, "pastes", strconv.Itoa(pasteID)+".json"), data, 0644)
if err != nil {
ff.writeMutex.Unlock()
return 0, err
}
// Add it to cache.
indexData := &Index{}
indexData.ID = pasteID
indexData.Private = p.Private
ff.pastesIndex = append(ff.pastesIndex, indexData)
ff.writeMutex.Unlock()
return int64(pasteID), nil
}
func (ff *FlatFiles) Shutdown() {
c.Logger.Info().Msg("Saving indexes...")
indexData, err := json.Marshal(ff.pastesIndex)
if err != nil {
c.Logger.Error().Msgf("Failed to encode index data into JSON: %s", err.Error())
return
}
err = ioutil.WriteFile(filepath.Join(ff.path, "pastes", "index.json"), indexData, 0644)
if err != nil {
c.Logger.Error().Msgf("Failed to write index data to file. Pretty sure that you've lost your pastes.")
return
}
}

View File

@@ -0,0 +1,63 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package flatfiles
import (
// stdlib
"database/sql"
// local
"github.com/pztrn/fastpastebin/pastes/model"
)
type Handler struct{}
func (dbh Handler) GetDatabaseConnection() *sql.DB {
return f.GetDatabaseConnection()
}
func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
return f.GetPaste(pasteID)
}
func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
return f.GetPagedPastes(page)
}
func (dbh Handler) GetPastesPages() int {
return f.GetPastesPages()
}
func (dbh Handler) Initialize() {
f.Initialize()
}
func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) {
return f.SavePaste(p)
}
func (dbh Handler) Shutdown() {
f.Shutdown()
}

View File

@@ -0,0 +1,31 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package flatfiles
// Index describes paste index structure for flatfiles data storage.
type Index struct {
ID int `yaml:"id"`
Private bool `json:"private"`
}

View File

@@ -0,0 +1,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 dialectinterface
import (
// stdlib
"database/sql"
// local
"github.com/pztrn/fastpastebin/pastes/model"
)
type Interface interface {
GetDatabaseConnection() *sql.DB
GetPaste(pasteID int) (*pastesmodel.Paste, error)
GetPagedPastes(page int) ([]pastesmodel.Paste, error)
GetPastesPages() int
Initialize()
SavePaste(p *pastesmodel.Paste) (int64, error)
Shutdown()
}

View File

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

View File

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

View File

@@ -0,0 +1,156 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package mysql
import (
// stdlib
"database/sql"
"fmt"
// local
"github.com/pztrn/fastpastebin/pastes/model"
// other
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
// Database is a MySQL/MariaDB connection controlling structure.
type Database struct {
db *sqlx.DB
}
func (db *Database) GetDatabaseConnection() *sql.DB {
return db.db.DB
}
// GetPaste returns a single paste by ID.
func (db *Database) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
p := &pastesmodel.Paste{}
err := db.db.Get(p, db.db.Rebind("SELECT * FROM `pastes` WHERE id=?"), pasteID)
if err != nil {
return nil, err
}
return p, nil
}
func (db *Database) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
var pastesRaw []pastesmodel.Paste
var pastes []pastesmodel.Paste
// Pagination.
var 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 ? OFFSET ?"), c.Config.Pastes.Pagination, startPagination)
if err != nil {
return nil, err
}
for i := range pastesRaw {
if !pastesRaw[i].IsExpired() {
pastes = append(pastes, pastesRaw[i])
}
}
return pastes, nil
}
func (db *Database) GetPastesPages() int {
var pastesRaw []pastesmodel.Paste
var pastes []pastesmodel.Paste
err := db.db.Get(&pastesRaw, "SELECT * FROM `pastes` WHERE private != true")
if err != nil {
return 1
}
// Check if pastes isn't expired.
for i := range pastesRaw {
if !pastesRaw[i].IsExpired() {
pastes = append(pastes, pastesRaw[i])
}
}
// Calculate pages.
pages := len(pastes) / 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...")
// There might be only user, without password. MySQL/MariaDB driver
// in DSN wants "user" or "user:password", "user:" is invalid.
var userpass = ""
if c.Config.Database.Password == "" {
userpass = c.Config.Database.Username
} else {
userpass = c.Config.Database.Username + ":" + c.Config.Database.Password
}
dbConnString := fmt.Sprintf("%s@tcp(%s:%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci&charset=utf8mb4", userpass, c.Config.Database.Address, c.Config.Database.Port, c.Config.Database.Database)
c.Logger.Debug().Msgf("Database connection string: %s", dbConnString)
dbConn, err := sqlx.Connect("mysql", dbConnString)
if err != nil {
c.Logger.Panic().Msgf("Failed to connect to database: %s", err.Error())
}
// Force UTC for current connection.
_ = dbConn.MustExec("SET @@session.time_zone='+00:00';")
c.Logger.Info().Msg("Database connection established")
db.db = dbConn
}
func (db *Database) SavePaste(p *pastesmodel.Paste) (int64, error) {
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
}
ID, err1 := result.LastInsertId()
if err1 != nil {
return 0, err
}
return ID, nil
}
func (db *Database) Shutdown() {
err := db.db.Close()
if err != nil {
c.Logger.Error().Msgf("Failed to close database connection: %s", err.Error())
}
}

View File

@@ -25,20 +25,47 @@
package database package database
import ( import (
// other // stdlib
"github.com/jmoiron/sqlx" "database/sql"
// local
"github.com/pztrn/fastpastebin/database/dialects/interface"
"github.com/pztrn/fastpastebin/pastes/model"
) )
// Handler is an interfaceable structure that proxifies calls from anyone // Handler is an interfaceable structure that proxifies calls from anyone
// to Database structure. // to Database structure.
type Handler struct{} type Handler struct{}
// GetDatabaseConnection returns current database connection. func (dbh Handler) GetDatabaseConnection() *sql.DB {
func (dbh Handler) GetDatabaseConnection() *sqlx.DB {
return d.GetDatabaseConnection() return d.GetDatabaseConnection()
} }
func (dbh Handler) GetPaste(pasteID int) (*pastesmodel.Paste, error) {
return d.GetPaste(pasteID)
}
func (dbh Handler) GetPagedPastes(page int) ([]pastesmodel.Paste, error) {
return d.GetPagedPastes(page)
}
func (dbh Handler) GetPastesPages() int {
return d.GetPastesPages()
}
// Initialize initializes connection to database. // Initialize initializes connection to database.
func (dbh Handler) Initialize() { func (dbh Handler) Initialize() {
d.Initialize() d.Initialize()
} }
func (dbh Handler) RegisterDialect(di dialectinterface.Interface) {
d.RegisterDialect(di)
}
func (dbh Handler) SavePaste(p *pastesmodel.Paste) (int64, error) {
return d.SavePaste(p)
}
func (dbh Handler) Shutdown() {
d.Shutdown()
}

View File

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

View File

@@ -54,8 +54,12 @@ func Migrate() {
// Add new migrations BEFORE this message. // Add new migrations BEFORE this message.
dbConn := c.Database.GetDatabaseConnection() dbConn := c.Database.GetDatabaseConnection()
err := goose.Up(dbConn.DB, ".") if dbConn != nil {
err := goose.Up(dbConn, ".")
if err != nil { if err != nil {
c.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error()) c.Logger.Panic().Msgf("Failed to migrate database to latest version: %s", err.Error())
} }
} else {
c.Logger.Warn().Msg("Current database dialect isn't supporting migrations, skipping")
}
} }

View File

@@ -1,6 +1,13 @@
# Database configuration. # Database configuration.
# Only MySQL database is supported for now. # Only MySQL database and flatfiles are supported for now.
database: database:
# Database type. The only supported ATM is "mysql" and "flatfiles".
type: "mysql"
# Path for data stored with "flatfiles" database adapter.
# Will be comletely ignored for MySQL/MariaDB.
path: "./data"
# Next parameters are strictly for MySQL/MariaDB connections and
# will be ignored by "flatfiles" adapter.
address: "localhost" address: "localhost"
port: "3306" port: "3306"
username: "fastpastebin" username: "fastpastebin"
@@ -18,9 +25,14 @@ logging:
# HTTP server configuration. # HTTP server configuration.
http: http:
address: "localhost" address: "192.168.0.14"
port: "25544" port: "25544"
# By default we're allowing only HTTPS requests. Setting this to true # By default we're allowing only HTTPS requests. Setting this to true
# will allow HTTP requests. Useful for developing or if you're # will allow HTTP requests. Useful for developing or if you're
# running Fast Pastebin behind reverse proxy that does SSL termination. # running Fast Pastebin behind reverse proxy that does SSL termination.
allow_insecure: false allow_insecure: true
# Pastes configuration.
pastes:
# Pastes per page.
pagination: 10

View File

@@ -95,7 +95,10 @@ custom:
# end: files # end: files
- files: - files:
- "assets/error.html" - "assets/error.html"
- "assets/footer.html"
- "assets/index.html" - "assets/index.html"
- "assets/main.html"
- "assets/navigation.html"
- "assets/pagination_ellipsis.html" - "assets/pagination_ellipsis.html"
- "assets/pagination_link_current.html" - "assets/pagination_link_current.html"
- "assets/pagination_link.html" - "assets/pagination_link.html"

View File

@@ -34,9 +34,10 @@ import (
"time" "time"
// local // local
"github.com/pztrn/fastpastebin/api/http/static"
"github.com/pztrn/fastpastebin/captcha" "github.com/pztrn/fastpastebin/captcha"
"github.com/pztrn/fastpastebin/pagination" "github.com/pztrn/fastpastebin/pagination"
"github.com/pztrn/fastpastebin/pastes/model"
"github.com/pztrn/fastpastebin/templater"
// other // other
"github.com/alecthomas/chroma" "github.com/alecthomas/chroma"
@@ -54,11 +55,6 @@ var (
// GET for "/paste/PASTE_ID" and "/paste/PASTE_ID/TIMESTAMP" (private pastes). // GET for "/paste/PASTE_ID" and "/paste/PASTE_ID/TIMESTAMP" (private pastes).
func pasteGET(ec echo.Context) error { func pasteGET(ec echo.Context) error {
errhtml, err := static.ReadFile("error.html")
if err != nil {
return ec.String(http.StatusNotFound, "error.html wasn't found!")
}
pasteIDRaw := ec.Param("id") pasteIDRaw := ec.Param("id")
// We already get numbers from string, so we will not check strconv.Atoi() // We already get numbers from string, so we will not check strconv.Atoi()
// error. // error.
@@ -66,17 +62,17 @@ func pasteGET(ec echo.Context) error {
c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID) c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
// Get paste. // Get paste.
paste, err1 := GetByID(pasteID) paste, err1 := c.Database.GetPaste(pasteID)
if err1 != nil { if err1 != nil {
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error()) c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1) errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
if paste.IsExpired() { if paste.IsExpired() {
c.Logger.Error().Msgf("Paste #%d is expired", pasteID) c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1) errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
// Check if we have a private paste and it's parameters are correct. // Check if we have a private paste and it's parameters are correct.
@@ -85,14 +81,14 @@ func pasteGET(ec echo.Context) error {
tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64) tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64)
if err2 != nil { if err2 != nil {
c.Logger.Error().Msgf("Invalid timestamp '%s' provided for getting private paste #%d: %s", tsProvidedStr, pasteID, err2.Error()) c.Logger.Error().Msgf("Invalid timestamp '%s' provided for getting private paste #%d: %s", tsProvidedStr, pasteID, err2.Error())
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1) errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
pasteTs := paste.CreatedAt.Unix() pasteTs := paste.CreatedAt.Unix()
if tsProvided != pasteTs { if tsProvided != pasteTs {
c.Logger.Error().Msgf("Incorrect timestamp '%v' provided for private paste #%d, waiting for %v", tsProvidedStr, pasteID, strconv.FormatInt(pasteTs, 10)) c.Logger.Error().Msgf("Incorrect timestamp '%v' provided for private paste #%d, waiting for %v", tsProvidedStr, pasteID, strconv.FormatInt(pasteTs, 10))
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1) errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
} }
@@ -117,24 +113,20 @@ func pasteGET(ec echo.Context) error {
// If all okay - do nothing :) // If all okay - do nothing :)
} }
pasteHTML, err2 := static.ReadFile("paste.html") // Format paste data map.
if err2 != nil { pasteData := make(map[string]string)
return ec.String(http.StatusNotFound, "parse.html wasn't found!") pasteData["pasteTitle"] = paste.Title
} pasteData["pasteID"] = strconv.Itoa(paste.ID)
pasteData["pasteDate"] = paste.CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
// Format template with paste data. pasteData["pasteExpiration"] = paste.GetExpirationTime().Format("2006-01-02 @ 15:04:05") + " UTC"
pasteHTMLAsString := strings.Replace(string(pasteHTML), "{pasteTitle}", paste.Title, 1) pasteData["pasteLanguage"] = paste.Language
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteID}", strconv.Itoa(paste.ID), -1)
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteDate}", paste.CreatedAt.Format("2006-01-02 @ 15:04:05"), 1)
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteExpiration}", paste.GetExpirationTime().Format("2006-01-02 @ 15:04:05"), 1)
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteLanguage}", paste.Language, 1)
if paste.Private { if paste.Private {
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteType}", "<span class='has-text-danger'>Private</span>", 1) pasteData["pasteType"] = "<span class='has-text-danger'>Private</span>"
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteTs}", strconv.FormatInt(paste.CreatedAt.Unix(), 10)+"/", 1) pasteData["pasteTs"] = strconv.FormatInt(paste.CreatedAt.Unix(), 10) + "/"
} else { } else {
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteType}", "<span class='has-text-success'>Public</span>", 1) pasteData["pasteType"] = "<span class='has-text-success'>Public</span>"
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteTs}", "", 1) pasteData["pasteTs"] = ""
} }
// Highlight. // Highlight.
@@ -164,26 +156,16 @@ func pasteGET(ec echo.Context) error {
if err4 != nil { if err4 != nil {
c.Logger.Error().Msgf("Failed to format paste data: %s", err4.Error()) c.Logger.Error().Msgf("Failed to format paste data: %s", err4.Error())
} }
pasteData["pastedata"] = buf.String()
// Escape paste data. // Get template and format it.
//pasteData := html.EscapeString(buf.String()) pasteHTML := templater.GetTemplate(ec, "paste.html", pasteData)
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pastedata}", buf.String(), 1)
return ec.HTML(http.StatusOK, string(pasteHTMLAsString)) return ec.HTML(http.StatusOK, pasteHTML)
} }
// GET for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page. // GET for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
func pastePasswordedVerifyGet(ec echo.Context) error { func pastePasswordedVerifyGet(ec echo.Context) error {
verifyHTMLRaw, err := static.ReadFile("passworded_paste_verify.html")
if err != nil {
return ec.String(http.StatusNotFound, "passworded_paste_verify.html wasn't found!")
}
errhtml, err := static.ReadFile("error.html")
if err != nil {
return ec.String(http.StatusNotFound, "error.html wasn't found!")
}
pasteIDRaw := ec.Param("id") pasteIDRaw := ec.Param("id")
timestampRaw := ec.Param("timestamp") timestampRaw := ec.Param("timestamp")
// We already get numbers from string, so we will not check strconv.Atoi() // We already get numbers from string, so we will not check strconv.Atoi()
@@ -191,11 +173,11 @@ func pastePasswordedVerifyGet(ec echo.Context) error {
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0]) pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
// Get paste. // Get paste.
paste, err1 := GetByID(pasteID) paste, err1 := c.Database.GetPaste(pasteID)
if err1 != nil { if err1 != nil {
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error()) c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1) errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDRaw+" not found")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
// Check for auth cookie. If present - redirect to paste. // Check for auth cookie. If present - redirect to paste.
@@ -215,19 +197,18 @@ func pastePasswordedVerifyGet(ec echo.Context) error {
c.Logger.Debug().Msg("Invalid cookie, showing auth page") c.Logger.Debug().Msg("Invalid cookie, showing auth page")
} }
verifyHTML := strings.Replace(string(verifyHTMLRaw), "{pasteID}", strconv.Itoa(pasteID), -1) // HTML data.
verifyHTML = strings.Replace(verifyHTML, "{pasteTimestamp}", timestampRaw, 1) 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) return ec.HTML(http.StatusOK, verifyHTML)
} }
// POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page. // POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
func pastePasswordedVerifyPost(ec echo.Context) error { func pastePasswordedVerifyPost(ec echo.Context) error {
errhtml, err := static.ReadFile("error.html")
if err != nil {
return ec.String(http.StatusNotFound, "error.html wasn't found!")
}
pasteIDRaw := ec.Param("id") pasteIDRaw := ec.Param("id")
timestampRaw := ec.Param("timestamp") timestampRaw := ec.Param("timestamp")
// We already get numbers from string, so we will not check strconv.Atoi() // We already get numbers from string, so we will not check strconv.Atoi()
@@ -236,17 +217,18 @@ func pastePasswordedVerifyPost(ec echo.Context) error {
c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID) c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
// Get paste. // Get paste.
paste, err1 := GetByID(pasteID) paste, err1 := c.Database.GetPaste(pasteID)
if err1 != nil { if err1 != nil {
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error()) c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1) errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
params, err2 := ec.FormParams() params, err2 := ec.FormParams()
if err2 != nil { if err2 != nil {
c.Logger.Debug().Msg("No form parameters passed") c.Logger.Debug().Msg("No form parameters passed")
return ec.HTML(http.StatusBadRequest, string(errhtml)) errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
return ec.HTML(http.StatusBadRequest, errtpl)
} }
if paste.VerifyPassword(params["paste-password"][0]) { if paste.VerifyPassword(params["paste-password"][0]) {
@@ -261,46 +243,41 @@ func pastePasswordedVerifyPost(ec echo.Context) error {
return ec.Redirect(http.StatusFound, "/paste/"+strconv.Itoa(pasteID)+"/"+timestampRaw) return ec.Redirect(http.StatusFound, "/paste/"+strconv.Itoa(pasteID)+"/"+timestampRaw)
} }
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Invalid password. Please, try again.", 1) errtpl := templater.GetErrorTemplate(ec, "Invalid password. Please, try again.")
return ec.HTML(http.StatusBadRequest, string(errhtmlAsString)) return ec.HTML(http.StatusBadRequest, string(errtpl))
} }
// POST for "/paste/" which will create new paste and redirect to // POST for "/paste/" which will create new paste and redirect to
// "/pastes/CREATED_PASTE_ID". // "/pastes/CREATED_PASTE_ID".
func pastePOST(ec echo.Context) error { func pastePOST(ec echo.Context) error {
errhtml, err := static.ReadFile("error.html")
if err != nil {
return ec.String(http.StatusNotFound, "error.html wasn't found!")
}
params, err := ec.FormParams() params, err := ec.FormParams()
if err != nil { if err != nil {
c.Logger.Debug().Msg("No form parameters passed") errtpl := templater.GetErrorTemplate(ec, "Cannot create empty paste")
return ec.HTML(http.StatusBadRequest, string(errhtml)) return ec.HTML(http.StatusBadRequest, errtpl)
} }
c.Logger.Debug().Msgf("Received parameters: %+v", params) c.Logger.Debug().Msgf("Received parameters: %+v", params)
// Do nothing if paste contents is empty. // Do nothing if paste contents is empty.
if len(params["paste-contents"][0]) == 0 { if len(params["paste-contents"][0]) == 0 {
c.Logger.Debug().Msg("Empty paste submitted, ignoring") c.Logger.Debug().Msg("Empty paste submitted, ignoring")
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Empty pastes aren't allowed.", 1) errtpl := templater.GetErrorTemplate(ec, "Empty pastes aren't allowed.")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
if !strings.ContainsAny(params["paste-keep-for"][0], "Mmhd") { 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]) c.Logger.Debug().Msgf("'Keep paste for' field have invalid value: %s", params["paste-keep-for"][0])
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).", 1) errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
// Verify captcha. // Verify captcha.
if !captcha.Verify(params["paste-captcha-id"][0], params["paste-captcha-solution"][0]) { 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]) c.Logger.Debug().Msgf("Invalid captcha solution for captcha ID '%s': %s", params["paste-captcha-id"][0], params["paste-captcha-solution"][0])
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Invalid captcha solution.", 1) errtpl := templater.GetErrorTemplate(ec, "Invalid captcha solution.")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
paste := &Paste{ paste := &pastesmodel.Paste{
Title: params["paste-title"][0], Title: params["paste-title"][0],
Data: params["paste-contents"][0], Data: params["paste-contents"][0],
Language: params["paste-language"][0], Language: params["paste-language"][0],
@@ -319,13 +296,13 @@ func pastePOST(ec echo.Context) error {
keepFor, err1 := strconv.Atoi(keepForRaw) keepFor, err1 := strconv.Atoi(keepForRaw)
if err1 != nil { if err1 != nil {
c.Logger.Debug().Msgf("Failed to parse 'Keep for' integer: %s", err1.Error()) c.Logger.Debug().Msgf("Failed to parse 'Keep for' integer: %s", err1.Error())
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).", 1) errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
paste.KeepFor = keepFor paste.KeepFor = keepFor
keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0] keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0]
keepForUnit := PASTE_KEEPS_CORELLATION[keepForUnitRaw] keepForUnit := pastesmodel.PASTE_KEEPS_CORELLATION[keepForUnitRaw]
paste.KeepForUnitType = keepForUnit paste.KeepForUnitType = keepForUnit
// Try to autodetect if it was selected. // Try to autodetect if it was selected.
@@ -342,19 +319,19 @@ func pastePOST(ec echo.Context) error {
paste.Private = false paste.Private = false
privateCheckbox, privateCheckboxFound := params["paste-private"] privateCheckbox, privateCheckboxFound := params["paste-private"]
pastePassword, pastePasswordFound := params["paste-password"] pastePassword, pastePasswordFound := params["paste-password"]
if privateCheckboxFound && privateCheckbox[0] == "on" || pastePasswordFound && len(pastePassword[0]) != 0 { if privateCheckboxFound && privateCheckbox[0] == "on" || pastePasswordFound && pastePassword[0] != "" {
paste.Private = true paste.Private = true
} }
if len(pastePassword) != 0 { if pastePassword[0] != "" {
paste.CreatePassword(pastePassword[0]) paste.CreatePassword(pastePassword[0])
} }
id, err2 := Save(paste) id, err2 := c.Database.SavePaste(paste)
if err2 != nil { if err2 != nil {
c.Logger.Debug().Msgf("Failed to save paste: %s", err2.Error()) c.Logger.Debug().Msgf("Failed to save paste: %s", err2.Error())
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Failed to save paste. Please, try again later.", 1) errtpl := templater.GetErrorTemplate(ec, "Failed to save paste. Please, try again later.")
return ec.HTML(http.StatusBadRequest, errhtmlAsString) return ec.HTML(http.StatusBadRequest, errtpl)
} }
newPasteIDAsString := strconv.FormatInt(id, 10) newPasteIDAsString := strconv.FormatInt(id, 10)
@@ -377,7 +354,7 @@ func pasteRawGET(ec echo.Context) error {
c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID) c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
// Get paste. // Get paste.
paste, err1 := GetByID(pasteID) paste, err1 := c.Database.GetPaste(pasteID)
if err1 != nil { if err1 != nil {
c.Logger.Error().Msgf("Failed to get paste #%d from database: %s", pasteID, err1.Error()) 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.") return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
@@ -415,16 +392,6 @@ func pasteRawGET(ec echo.Context) error {
// GET for "/pastes/", a list of publicly available pastes. // GET for "/pastes/", a list of publicly available pastes.
func pastesGET(ec echo.Context) error { func pastesGET(ec echo.Context) error {
pasteListHTML, err1 := static.ReadFile("pastelist_list.html")
if err1 != nil {
return ec.String(http.StatusNotFound, "pastelist_list.html wasn't found!")
}
pasteElementHTML, err2 := static.ReadFile("pastelist_paste.html")
if err2 != nil {
return ec.String(http.StatusNotFound, "pastelist_paste.html wasn't found!")
}
pageFromParamRaw := ec.Param("page") pageFromParamRaw := ec.Param("page")
var page = 1 var page = 1
if pageFromParamRaw != "" { if pageFromParamRaw != "" {
@@ -435,7 +402,7 @@ func pastesGET(ec echo.Context) error {
c.Logger.Debug().Msgf("Requested page #%d", page) c.Logger.Debug().Msgf("Requested page #%d", page)
// Get pastes IDs. // Get pastes IDs.
pastes, err3 := GetPagedPastes(page) pastes, err3 := c.Database.GetPagedPastes(page)
c.Logger.Debug().Msgf("Got %d pastes", len(pastes)) c.Logger.Debug().Msgf("Got %d pastes", len(pastes))
var pastesString = "No pastes to show." var pastesString = "No pastes to show."
@@ -443,16 +410,17 @@ func pastesGET(ec echo.Context) error {
// Show "No pastes to show" on any error for now. // Show "No pastes to show" on any error for now.
if err3 != nil { if err3 != nil {
c.Logger.Error().Msgf("Failed to get pastes list from database: %s", err3.Error()) c.Logger.Error().Msgf("Failed to get pastes list from database: %s", err3.Error())
pasteListHTMLAsString := strings.Replace(string(pasteListHTML), "{pastes}", pastesString, 1) noPastesToShowTpl := templater.GetErrorTemplate(ec, "No pastes to show.")
return ec.HTML(http.StatusOK, string(pasteListHTMLAsString)) return ec.HTML(http.StatusOK, noPastesToShowTpl)
} }
if len(pastes) > 0 { if len(pastes) > 0 {
pastesString = "" pastesString = ""
for i := range pastes { for i := range pastes {
pasteString := strings.Replace(string(pasteElementHTML), "{pasteID}", strconv.Itoa(pastes[i].ID), 2) pasteDataMap := make(map[string]string)
pasteString = strings.Replace(pasteString, "{pasteTitle}", pastes[i].Title, 1) pasteDataMap["pasteID"] = strconv.Itoa(pastes[i].ID)
pasteString = strings.Replace(pasteString, "{pasteDate}", pastes[i].CreatedAt.Format("2006-01-02 @ 15:04:05"), 1) 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. // Get max 4 lines of each paste.
pasteDataSplitted := strings.Split(pastes[i].Data, "\n") pasteDataSplitted := strings.Split(pastes[i].Data, "\n")
@@ -462,18 +430,20 @@ func pastesGET(ec echo.Context) error {
} else { } else {
pasteData = strings.Join(pasteDataSplitted[0:4], "\n") pasteData = strings.Join(pasteDataSplitted[0:4], "\n")
} }
pasteString = strings.Replace(pasteString, "{pasteData}", pasteData, 1)
pastesString += pasteString pasteDataMap["pasteData"] = pasteData
pasteTpl := templater.GetRawTemplate(ec, "pastelist_paste.html", pasteDataMap)
pastesString += pasteTpl
} }
} }
pasteListHTMLAsString := strings.Replace(string(pasteListHTML), "{pastes}", pastesString, 1) // Pagination.
pages := c.Database.GetPastesPages()
pages := GetPastesPages()
c.Logger.Debug().Msgf("Total pages: %d, current: %d", pages, page) c.Logger.Debug().Msgf("Total pages: %d, current: %d", pages, page)
paginationHTML := pagination.CreateHTML(page, pages, "/pastes/") paginationHTML := pagination.CreateHTML(page, pages, "/pastes/")
pasteListHTMLAsString = strings.Replace(pasteListHTMLAsString, "{pagination}", paginationHTML, -1)
return ec.HTML(http.StatusOK, string(pasteListHTMLAsString)) pasteListTpl := templater.GetTemplate(ec, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML})
return ec.HTML(http.StatusOK, string(pasteListTpl))
} }

View File

@@ -22,7 +22,7 @@
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package pastes package pastesmodel
import ( import (
// stdlib // stdlib
@@ -55,16 +55,16 @@ var (
// Paste represents paste itself. // Paste represents paste itself.
type Paste struct { type Paste struct {
ID int `db:"id"` ID int `db:"id" json:"id"`
Title string `db:"title"` Title string `db:"title" json:"title"`
Data string `db:"data"` Data string `db:"data" json:"data"`
CreatedAt *time.Time `db:"created_at"` CreatedAt *time.Time `db:"created_at" json:"created_at"`
KeepFor int `db:"keep_for"` KeepFor int `db:"keep_for" json:"keep_for"`
KeepForUnitType int `db:"keep_for_unit_type"` KeepForUnitType int `db:"keep_for_unit_type" json:"keep_for_unit_type"`
Language string `db:"language"` Language string `db:"language" json:"language"`
Private bool `db:"private"` Private bool `db:"private" json:"private"`
Password string `db:"password"` Password string `db:"password" json:"password"`
PasswordSalt string `db:"password_salt"` PasswordSalt string `db:"password_salt" json:"password_salt"`
} }
// CreatePassword creates password for current paste. // CreatePassword creates password for current paste.

View File

@@ -1,115 +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
const (
// Pagination. Hardcoded for 10 for now.
PAGINATION = 10
)
// GetByID returns a single paste by ID.
func GetByID(id int) (*Paste, error) {
p := &Paste{}
dbConn := c.Database.GetDatabaseConnection()
err := dbConn.Get(p, dbConn.Rebind("SELECT * FROM `pastes` WHERE id=?"), id)
if err != nil {
return nil, err
}
// Lets go with checking.
return p, nil
}
// GetPagedPastes returns a paged slice of pastes.
func GetPagedPastes(page int) ([]Paste, error) {
var pastesRaw []Paste
var pastes []Paste
dbConn := c.Database.GetDatabaseConnection()
// Pagination - 10 pastes on page.
var startPagination = 0
if page > 1 {
startPagination = (page - 1) * PAGINATION
}
err := dbConn.Select(&pastesRaw, dbConn.Rebind("SELECT * FROM `pastes` WHERE private != true ORDER BY id DESC LIMIT ? OFFSET ?"), PAGINATION, startPagination)
if err != nil {
return nil, err
}
for i := range pastesRaw {
if !pastesRaw[i].IsExpired() {
pastes = append(pastes, pastesRaw[i])
}
}
return pastes, nil
}
// GetPastesPages returns an integer that represents quantity of pages
// that can be requested (or drawn in paginator).
func GetPastesPages() int {
var pastesRaw []Paste
var pastes []Paste
dbConn := c.Database.GetDatabaseConnection()
err := dbConn.Get(&pastesRaw, "SELECT * FROM `pastes` WHERE private != true")
if err != nil {
return 1
}
// Check if pastes isn't expired.
for i := range pastesRaw {
if !pastesRaw[i].IsExpired() {
pastes = append(pastes, pastesRaw[i])
}
}
// Calculate pages.
pages := len(pastes) / PAGINATION
// Check if we have any remainder. Add 1 to pages count if so.
if len(pastes)%PAGINATION > 0 {
pages++
}
return pages
}
// Save saves paste to database and returns it's ID.
func Save(p *Paste) (int64, error) {
dbConn := c.Database.GetDatabaseConnection()
result, err := dbConn.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
}
ID, err1 := result.LastInsertId()
if err1 != nil {
return 0, err
}
return ID, nil
}

124
templater/exported.go Normal file
View File

@@ -0,0 +1,124 @@
// Fast Paste Bin - uberfast and easy-to-use pastebin.
//
// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
// developers.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject
// to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package templater
import (
// stdlib
"net/http"
"strings"
// local
"github.com/pztrn/fastpastebin/api/http/static"
"github.com/pztrn/fastpastebin/context"
// other
"github.com/labstack/echo"
)
var (
c *context.Context
)
// GetErrorTemplate returns formatted error template.
// If error.html wasn't found - it will return "error.html not found"
// message as simple string.
func GetErrorTemplate(ec echo.Context, errorText string) string {
// Getting main error template.
mainhtml := GetTemplate(ec, "error.html", map[string]string{"error": errorText})
return mainhtml
}
// GetRawTemplate returns only raw template data.
func GetRawTemplate(ec echo.Context, templateName string, data map[string]string) string {
// Getting main template.
tplRaw, err := static.ReadFile(templateName)
if err != nil {
ec.String(http.StatusBadRequest, templateName+" not found.")
return ""
}
tpl := string(tplRaw)
// Replace placeholders with data from data map.
for placeholder, value := range data {
tpl = strings.Replace(tpl, "{"+placeholder+"}", value, -1)
}
return tpl
}
// GetTemplate returns formatted template that can be outputted to client.
func GetTemplate(ec echo.Context, name string, data map[string]string) string {
c.Logger.Debug().Msgf("Requested template '%s'", name)
// Getting main template.
mainhtml, err := static.ReadFile("main.html")
if err != nil {
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.")
return ""
}
// Getting footer.
footerhtml, err2 := static.ReadFile("footer.html")
if err2 != nil {
ec.String(http.StatusBadRequest, "footer.html not found.")
return ""
}
// Format main template.
tpl := strings.Replace(string(mainhtml), "{navigation}", string(navhtml), 1)
tpl = strings.Replace(tpl, "{footer}", string(footerhtml), 1)
// Version.
tpl = strings.Replace(tpl, "{version}", context.Version, 1)
// Get requested template.
reqhtml, err3 := static.ReadFile(name)
if err3 != nil {
ec.String(http.StatusBadRequest, name+" not found.")
return ""
}
// Replace documentBody.
tpl = strings.Replace(tpl, "{documentBody}", string(reqhtml), 1)
// Replace placeholders with data from data map.
for placeholder, value := range data {
tpl = strings.Replace(tpl, "{"+placeholder+"}", value, -1)
}
return tpl
}
// Initialize initializes package.
func Initialize(cc *context.Context) {
c = cc
}