Started working on V2 rework

This commit is contained in:
Daniel Legt 2022-05-18 22:54:07 +03:00
parent 2d7d6a2f8b
commit 89eaceddcc
57 changed files with 22 additions and 680 deletions

View File

@ -1,5 +0,0 @@
dev
.gitignore
LICENSE
README.md
dist/main.db

View File

@ -9,15 +9,3 @@ DOMAIN_BASE=http://localhost:8080
CACHE_MAP_LIMIT=15 CACHE_MAP_LIMIT=15
# Maximum API call requests to the API, this will ban the api for 5 minutes after requesting more than API_RATE_LIMIT in 5 minutes. # Maximum API call requests to the API, this will ban the api for 5 minutes after requesting more than API_RATE_LIMIT in 5 minutes.
API_BAN_LIMIT=300 API_BAN_LIMIT=300
# Optional database driver, you can just ignore and use sqlite
# ! Warning: Sqlite not implemented yet
DATABASE_DRIVER=mariadb
# Mysql database connetion details, fill these in if you chose mysql above.
MYSQL_ROOT_PASSWORD=example-dev
MYSQL_DATABASE=freepad
MYSQL_USER=freepad
MYSQL_PASSWORD=example-dev
MYSQL_URL=mariadb
MYSQL_PORT=3306

6
.gitignore vendored
View File

@ -1,8 +1,4 @@
dev/* dev/*
!dev/.keep !dev/.keep
.env .env
docker-compose.yaml docker-compose.yaml
# Ignore the database file
dist/main.db
dist/freepad
dist/freepad

View File

@ -1,15 +0,0 @@
FROM alpine
LABEL version="1.5.1"
# Copy the distribution files
COPY ./dist /app
# Make /app the work directory
WORKDIR /app
# Expose the listening port
EXPOSE 8080
# Run the program
ENTRYPOINT ["./freepad"]

View File

@ -8,7 +8,7 @@ Quickly create "pads" and share with others
[![demo](https://img.shields.io/badge/Demo-Check%20out%20the%20functionality-orange)](https://pad.justkato.me/) [![demo](https://img.shields.io/badge/Demo-Check%20out%20the%20functionality-orange)](https://pad.justkato.me/)
![MariaDB](https://img.shields.io/badge/MariaDB-003545?style=for-the-badge&logo=mariadb&logoColor=white) ![MariaDB](https://img.shields.io/badge/MariaDB-003545?style=for-the-badge&logo=mariadb&logoColor=white)
# **FreePad** # **FreePad V2**
**FreePad** is a simple `Go` project to help you juggle temporary notes that you might wanna pass from one device to another, or from a person to another with memorable and easy to communicate online "Pads". **FreePad** is a simple `Go` project to help you juggle temporary notes that you might wanna pass from one device to another, or from a person to another with memorable and easy to communicate online "Pads".
The project is absolutely free to use, you can extend the code and even contribute, I am more than happy to be corrected in my horrible beginner code. The project is absolutely free to use, you can extend the code and even contribute, I am more than happy to be corrected in my horrible beginner code.

View File

@ -1,15 +0,0 @@
#!/bin/sh
echo "Removing old";
rm dist/freepad;
# Remember current path
MYDIR=`pwd`;
# Go into src
cd src;
# Build
echo "Building..."
GIN_MODE=release CGO_ENABLED=0 GOOS=linux GIN_MODE=release go build -a -installsuffix cgo -o ../dist/freepad .
# Go back!
cd $MYDIR;

View File

@ -1 +0,0 @@
DROP TABLE IF EXISTS t_posts;

View File

@ -1,12 +0,0 @@
CREATE TABLE IF NOT EXISTS `t_posts` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(256) NOT NULL DEFAULT '' COLLATE 'latin1_swedish_ci',
`content` MEDIUMTEXT NOT NULL COLLATE 'latin1_swedish_ci',
`ts` DATETIME NOT NULL DEFAULT current_timestamp(),
`ts_updated` DATETIME NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name` (`name`) USING BTREE
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB
;

View File

@ -1 +0,0 @@
DROP table t_posts;

View File

@ -1,6 +0,0 @@
CREATE TABLE IF NOT EXISTS "t_posts" (
"id" INTEGER,
"name" TEXT,
"content" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);

View File

View File

@ -1,38 +0,0 @@
version: '3.4'
services:
freepad:
build: .
networks:
- backend
environment:
- DOMAIN_BASE=http://localhost:8888
- MYSQL_URL=mariadb
- MYSQL_ROOT_PASSWORD
- MYSQL_DATABASE
- MYSQL_USER
- MYSQL_PASSWORD
- MYSQL_PORT
depends_on:
- mariadb
ports:
- 8888:8080
mariadb:
image: mariadb:10.2
environment:
- MYSQL_ROOT_PASSWORD
- MYSQL_DATABASE
- MYSQL_USER
- MYSQL_PASSWORD
restart: unless-stopped
networks:
- backend
volumes:
- ./dev/mariadb:/var/lib/mysql
ports:
- 3306:3306
networks:
backend:
driver: bridge

View File

View File

20
main.go Normal file
View File

@ -0,0 +1,20 @@
package main
import (
"os"
"github.com/gin-gonic/gin"
)
func main() {
_, isDevelopment := os.LookupEnv("DEV_MODE")
if isDevelopment {
gin.SetMode(gin.ReleaseMode)
}
// Initialize the router
router := gin.Default()
router.Run(":8080")
}

View File

@ -1,23 +0,0 @@
#!/bin/bash
echo "Removing old";
rm dist/freepad;
source ../.env
# Yeah, this is my solution
export DOMAIN_BASE CACHE_MAP_LIMIT API_BAN_LIMIT DATABASE_DRIVER MYSQL_ROOT_PASSWORD MYSQL_DATABASE MYSQL_USER MYSQL_PASSWORD MYSQL_URL MYSQL_PORT IS_DEV
# Remember current path
MYDIR=`pwd`;
# Go into src
cd src;
# Build
echo "Building..."
go build -o ../dist/freepad .
# Go back!
cd $MYDIR;
cd dist
./freepad && cd $MYDIR;

0
src/.gitignore vendored
View File

View File

@ -1,122 +0,0 @@
package post
import (
"errors"
"github.com/JustKato/FreePad/helper"
"github.com/JustKato/FreePad/models/database"
)
var postList []*Post = []*Post{}
var postMap map[string]Post = make(map[string]Post)
func GetPostList() []*Post {
return postList
}
func Retrieve(name string) (*Post, error) {
if len(name) < 1 {
return nil, errors.New("the name of the post must contain at least 1 character")
}
if len(name) > 256 {
return nil, errors.New("the name of the post must not exceed 256 characters")
}
// Check if we have the post cached
if val, ok := postMap[name]; ok {
return &val, nil
}
// Add the post to the database
db, err := database.GetConn()
if err != nil {
println("Erorr", err)
return nil, err
}
defer db.Close()
sql := `SELECT p.name, p.content FROM freepad.t_posts p WHERE p.name = ? LIMIT 1;`
s, err := db.Prepare(sql)
if err != nil {
return nil, err
}
rows, err := s.Query(name)
if err != nil {
return nil, err
}
defer rows.Close()
anyLeft := rows.Next()
if !anyLeft {
return nil, errors.New("could not find the requested post")
}
foundPost := Post{
Name: "",
Content: "",
}
err = rows.Scan(&foundPost.Name, &foundPost.Content)
if err != nil {
return nil, err
}
return &foundPost, nil
}
func Create(name string, content string) (*Post, error) {
if len(name) < 1 {
return nil, errors.New("the name of the post must contain at least 1 character")
}
if len(name) > 256 {
return nil, errors.New("the name of the post must not exceed 256 characters")
}
if len(content) > 16777200 {
return nil, errors.New("provided content is too long, please do not exceed ")
}
// Initialize the post
myPost := Post{
Name: name,
Content: content,
}
// Check if we can cache this element
if len(postMap) > helper.GetCacheMapLimit() {
// Reset Cache
postMap = make(map[string]Post)
}
// Set the post by name
postMap[name] = myPost
// Add the post to the database
db, err := database.GetConn()
if err != nil {
return nil, err
}
defer db.Close()
sql := `REPLACE INTO freepad.t_posts (name, content) VALUES (?, ?)`
s, err := db.Prepare(sql)
if err != nil {
return nil, err
}
_, err = s.Exec(myPost.Name, myPost.Content)
if err != nil {
return nil, err
}
// Return the post
return &myPost, nil
}

View File

@ -1,6 +0,0 @@
package post
type Post struct {
Name string `json:"name"`
Content string `json:"content"`
}

View File

@ -1,28 +0,0 @@
package helper
import (
"time"
"github.com/gin-gonic/gin"
"github.com/ulule/limiter/v3"
"github.com/ulule/limiter/v3/drivers/store/memory"
mgin "github.com/ulule/limiter/v3/drivers/middleware/gin"
)
func BindRateLimiter(router *gin.RouterGroup) {
// Setup rate limitng
rate := limiter.Rate{
Period: 5 * time.Minute,
Limit: 150,
}
// Initialize the memory storage
store := memory.NewStore()
// Generate the middleware
middleware := mgin.NewMiddleware(limiter.New(store, rate))
// Use the middleware
router.Use(middleware)
}

View File

@ -1,33 +0,0 @@
package helper
import (
"os"
"strconv"
)
func GetDomainBase() string {
domainBase, domainExists := os.LookupEnv("DOMAIN_BASE")
if !domainExists {
os.Setenv("DOMAIN_BASE", "http://localhost:8080")
domainBase = "http://localhost:8080"
}
return domainBase
}
func GetCacheMapLimit() int {
cacheMapLimit, domainExists := os.LookupEnv("CACHE_MAP_LIMIT")
if !domainExists {
os.Setenv("CACHE_MAP_LIMIT", "25")
cacheMapLimit = "25"
}
rez, err := strconv.Atoi(cacheMapLimit)
if err != nil {
return 25
}
return rez
}

View File

@ -1,45 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/JustKato/FreePad/models/database"
"github.com/JustKato/FreePad/routes"
"github.com/gin-gonic/gin"
)
func main() {
_, isDevelopment := os.LookupEnv("IS_DEV")
if isDevelopment {
gin.SetMode(gin.ReleaseMode)
}
// Initialize the router
router := gin.Default()
// Read HTML Templates
router.LoadHTMLGlob("templates/**/*.html")
// Load in static path
router.Static("/static", "static/")
// Add Routes
routes.HomeRoutes(router)
// Bind /api
routes.ApiRoutes(router.Group("/api"))
// TODO: Sockets: https://gist.github.com/supanadit/f6de65fc5896e8bb0c4656e451387d0f
// Try and run migrations
err := database.MigrateMysql()
if err != nil {
fmt.Println("Error")
fmt.Println(err)
fmt.Println("Error")
}
router.Run(":8080")
}

View File

@ -1,102 +0,0 @@
package database
import (
"database/sql"
"fmt"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
)
// Declare the default database driver
const defaultDatabaseDriver string = "sqlite"
// Declare the valid database drivers
var validDatabaseDrivers []string = []string{"sqlite", "mysql"}
// Get the database type to use
func getDbType() string {
// Grab the environment variable
db, test := os.LookupEnv(`DATABASE_DRIVER`)
// Check if the test has failed
if !test {
return defaultDatabaseDriver
}
for _, v := range validDatabaseDrivers {
// Check if the provided database corresponds to this entry
if v == db {
// This is a valid database type
return db
}
}
// No matches
return defaultDatabaseDriver
}
func GetConn() (*sql.DB, error) {
// TODO: Implement sqlite properly.
return GetMysqlConn()
// Check what kind of database we are looking for
// dbConnType := getDbType()
// if dbConnType == `mysql` {
// return GetMysqlConn()
// } else {
// return GetLiteConn()
// }
}
func GetSqliteDatabasePath() string {
return "main.db"
}
func GetLiteConn() (*sql.DB, error) {
// Declare the database file name
dbFile := GetSqliteDatabasePath()
db, err := sql.Open("sqlite3", dbFile)
if err != nil {
println("Error", err)
return nil, err
}
return db, nil
}
func GetMysqlString() string {
user := os.Getenv("MYSQL_USER")
password := os.Getenv("MYSQL_PASSWORD")
dburl := os.Getenv("MYSQL_URL")
dbname := os.Getenv("MYSQL_DATABASE")
return fmt.Sprintf("mysql://%s:%s@tcp(%s:3306)/%s", user, password, dburl, dbname)
}
func GetMysqlConn() (*sql.DB, error) {
user := os.Getenv("MYSQL_USER")
password := os.Getenv("MYSQL_PASSWORD")
dburl := os.Getenv("MYSQL_URL")
dbname := os.Getenv("MYSQL_DATABASE")
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s", user, password, dburl, dbname))
if err != nil {
return nil, err
}
// Set options
db.SetConnMaxLifetime(time.Minute * 5)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
return db, nil
}

View File

@ -1,55 +0,0 @@
package database
import (
"fmt"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/mysql"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func MigrateMysql() error {
m, err := migrate.New(
"file://db/migrations/",
GetMysqlString(),
)
if err != nil {
return err
}
// Migrate
err = m.Up()
if err != nil {
return err
}
return m.Run()
}
// Run migrations to ensure tables exist
func MigrationUpdate() *migrate.Logger {
// Get the path to the sqlite database
databasePath := fmt.Sprintf("sqlite://%s", GetSqliteDatabasePath())
// Try and create a new migration
m, err := migrate.New(
"file://../db/migrations_sqlite",
databasePath,
)
if err != nil {
// End the whole thing if migrations fail
panic(err)
}
// Run the update
err = m.Up()
m.Run()
m.Force(1)
return &m.Log
}

View File

@ -1,111 +0,0 @@
package routes
import (
"encoding/base64"
"fmt"
"net/url"
"github.com/JustKato/FreePad/controllers/post"
"github.com/JustKato/FreePad/helper"
"github.com/JustKato/FreePad/models/database"
"github.com/JustKato/FreePad/types"
"github.com/gin-gonic/gin"
"github.com/skip2/go-qrcode"
)
func ApiRoutes(route *gin.RouterGroup) {
// Bind the rate limiter
helper.BindRateLimiter(route)
route.POST("/post", func(ctx *gin.Context) {
// Get the name of the post
postName := ctx.PostForm("name")
// Get the content of the post
postContent := ctx.PostForm("content")
// Try and run migrations
err := database.MigrateMysql()
if err != nil {
fmt.Println("Error")
fmt.Println(err)
fmt.Println("Error")
}
// Create my post
myPost, err := post.Create(postName, postContent)
if err != nil {
fmt.Println("Error", err)
ctx.JSON(400, types.FreeError{
Error: err.Error(),
Message: "There has been an error processing your request",
})
return
}
ctx.JSON(200, gin.H{
"message": "Post succesfully created",
"post": myPost,
"link": fmt.Sprintf("%s/%s", helper.GetDomainBase(), url.QueryEscape(myPost.Name)),
})
})
route.GET("/post", func(ctx *gin.Context) {
// Get the name of the post
postName := ctx.PostForm("name")
myPost, err := post.Retrieve(postName)
if err != nil {
fmt.Println("Error", err)
ctx.JSON(400, types.FreeError{
Error: err.Error(),
Message: "There has been an error processing your request",
})
return
}
// Return the post list
ctx.JSON(200, myPost)
})
route.GET("/posts", func(ctx *gin.Context) {
// Return the post list
ctx.JSON(200, post.GetPostList())
})
// Add in health checks
route.GET("/health", healthCheck)
route.POST("/qr", func(ctx *gin.Context) {
// Get the name of the post
link := ctx.PostForm("link")
// store the png somewhere
var png []byte
// Encode the link into a qr code
png, err := qrcode.Encode(link, qrcode.High, 512)
if err != nil {
ctx.JSON(200, types.FreeError{
Error: fmt.Sprint(err),
Message: "Failed to convert qr Code",
})
return
}
// Write the png to the response
ctx.JSON(200, gin.H{
"message": "Succesfully generated the QR",
"qr": "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(png),
})
})
}
func healthCheck(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "Healthy",
})
}

View File

@ -1,38 +0,0 @@
package routes
import (
"github.com/JustKato/FreePad/controllers/post"
"github.com/JustKato/FreePad/helper"
"github.com/gin-gonic/gin"
)
func HomeRoutes(router *gin.Engine) {
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "HomePage",
"domain_base": helper.GetDomainBase(),
})
})
router.GET("/:post", func(c *gin.Context) {
// Get the post we are looking for.
postName := c.Param("post")
// Try and get this post's data
postData, err := post.Retrieve(postName)
if err != nil {
postData = &post.Post{
Name: postName,
Content: "",
}
}
c.HTML(200, "page.html", gin.H{
"title": postName,
"post_content": postData.Content,
"domain_base": helper.GetDomainBase(),
})
})
}

View File

@ -1,6 +0,0 @@
package types
type FreeError struct {
Error string `json:"error"`
Message string `json:"message"`
}

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 218 B

After

Width:  |  Height:  |  Size: 218 B

View File

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 780 B

View File

Before

Width:  |  Height:  |  Size: 1021 B

After

Width:  |  Height:  |  Size: 1021 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB