Merge pull request #1 from JustKato/v2

V2
This commit is contained in:
Kato 2022-05-20 02:00:29 +03:00 committed by GitHub
commit 9763b53053
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 678 additions and 2754 deletions

View File

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

View File

@ -4,20 +4,17 @@
DOMAIN_BASE=http://localhost:8080
## ? Functionality Changes, these are optional but recommended to setup as you like
# The maximum map caching, from my testing 15 would reach 250mb of ram usage on docker
CACHE_MAP_LIMIT=15
# The folder in which to store all of the pads, please leave a trailing slash
PAD_STORAGE_PATH=./data/
# 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
# Optional database driver, you can just ignore and use sqlite
# ! Warning: Sqlite not implemented yet
DATABASE_DRIVER=mariadb
# Wether or not to run it all in dev-mode
DEV_MODE=0
# 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
# Maximum file storage age in minutes, set to -1 to disable
CLEANUP_MAX_AGE=43200 # Default is a month

5
.gitignore vendored
View File

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

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

@ -1,4 +1,4 @@
![Gopher](./dist/static/img/twitter_header_photo_2.png)
![Gopher](static/img/twitter_header_photo_2.png)
Quickly create "pads" and share with others
@ -8,86 +8,26 @@ Quickly create "pads" and share with others
[![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)
# **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".
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 current maintainer and creator is `Kato Twofold`
![Gopher](./dist/static/img/banner_prerequisites.png)
## Why version 2?
Version 2 has been initialized to rewrite everything and applying what I have learned from v1, I am new to go and I haven't followed the best of best practices and I know for a fact I can do better, so V2 has been pushed.
![Gopher](static/img/banner_prerequisites.png)
Before getting started there are a couple things you should configure before proceeding, such as the database storage type and a couple limits, now if you really want to you can skip these but it's better to know what you're running as to not wake up with a not-so-nice surprise.
![Gopher](./dist/static/img/banner_environment.png)
![Gopher](static/img/banner_environment.png)
The `.env` file contains all of the available options and you should use it to change those said variables, these are really important to customizing and self hosting this experience for yourself.
If you need any help with any setting you can always open an issue over on github and get help from me.
If you are barely getting started with hosting your own services, or even Sys admin stuff in general or writing code my suggestion is to just copy `.env` and leave it as is until you get it running with the defaults running fine, afterwards you can play with it a little and who knows, maybe even get to learn something!
![Gopher](./dist/static/img/banner_building.png)
## From Source
Building from source isn't exactly recommended as it's a hasle
```bash
# Clone the repo
git clone https://github.com/JustKato/FreePad FreePad
# Get in it!
cd FreePad
# Install golang
sudo apt install golang # Obviously use your distro's package manager
# Run the build Script
./build.sh
# Check out the ./dist folder
cd ./dist
# Make sure you change settings here
cp ../.env.example ./.env
# Run the program
./freepad
```
## Running the Binary
```bash
# Download the latest version of FreePad
freepad.1.0.3.zip
# Extract to wherever
unzip freepad.1.0.3.zip
# Get into the directory
cd FreePad
# ( Optionaly but recommended ) Edit the .env file
vim .env
# Run the program
./freepad
```
## Starting with Docker-Compose [ WIP ]
```bash
# Copy the example docker-compose file to anywhere
wget https://raw.githubusercontent.com/JustKato/FreePad/master/docker-compose.example.yaml
# Yoink the example .env file while we're at it
wget https://raw.githubusercontent.com/JustKato/FreePad/master/.env.example
# Rename the files
mv docker-compose.example.yaml docker-compose.yaml
mv .env.example .env
# ! Please take a look at the files and edit them before running
docker-compose up -d;
```
If you are barely getting started with hosting your own services, or even Sys admin stuff in general or writing code my suggestion is to just copy `.env` and leave it as is until you get it running with the defaults running fine, afterwards you can play with it a little and who knows, maybe even get to learn something!

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

@ -1,3 +0,0 @@
#post_content {
height: calc(100vh - 35rem);
}

View File

View File

@ -1,20 +0,0 @@
{{ define "inc/footer.html"}}
<footer class="footer">
<div class="content has-text-centered">
<p>
<strong>FreePad</strong> by <a href="https://justkato.me">Kato Twofold</a>.
<br>
The source code is licensed under the
<a href="http://opensource.org/licenses/mit-license.php">MIT</a> License.
<br>
The project is <b>Free and Open Source</b>, check out our <a href="https://github.com/JustKato/FreePad">Github Repo</a>.
<br>
Powered by <b>Go</b>phers!
</p>
</div>
</footer>
<script src="/static/js/main.js"></script>
{{ end }}

View File

@ -1,22 +0,0 @@
{{ define "inc/header.html"}}
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FreePad - {{.title}}</title>
<meta name="description" content="Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time." />
<meta property="og:description" content="Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time." />
<meta property="fb:app_id" content="231493360234820" />
<meta property="og:title" content="FreePad.com - #1 paste tool since 2002!" />
<meta property="og:type" content="article" />
<meta property="og:site_name" content="FreePad" />
<meta property="og:url" content="{{.domain_base}}/" />
<link rel="icon" type="image/png" href="/static/img/gopher.png"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/bulma-prefers-dark" />
<link rel="stylesheet" href="/static/css/main.css">
</head>
{{ end }}

View File

@ -1,42 +0,0 @@
{{ template "inc/header.html" .}}
<body>
<div class="container is-fullhd p-4 mb-6" style="min-height: 50rem; height: calc(100vh - 18rem)">
<h1 class="title">FreePad</h1>
<h2 class="subtitle">Free and Open source internet notepad</h2>
<hr class="mb-6">
<div class="content">
<form class="columns px-4 mt-6" onsubmit="goToPost(); return false;">
<span class="mt-2 is-size-4" style="font-family: monospace">{{.domain_base}}/</span>
<input class="mt-2 input mr-4" type="text" name="postName" id="postName" style="font-family: monospace" placeholder="Something memorable">
<button class="mt-2 button" type="submit">Open</button>
</form>
</div>
<div class="content my-4">
<h3>Why FreePad</h3>
<p>Why should you use FreePad and not some other provider?</p>
<ol>
<li style="list-style-type: none;">Keep it simple 😎</li>
<li style="list-style-type: none;">Don't worry about passwords 📔</li>
<li style="list-style-type: none;">You can be a mess, it's all temporary 🗑</li>
<li style="list-style-type: none;">Quick and Clean 🧼</li>
</ol>
</div>
<div class="content my-6">
<h3>Some Rules</h3>
<p>FreePad has an API and you can use it to interact with it, although there are some limitations.</p>
<ol>
<li>You are going to be rate-limited if you spam too much and put on a cooldown</li>
<li>There's only so many pages that can be stored before affecting performance</li>
<li>Automatic Shadowbanning</li>
</ol>
</div>
</div>
{{ template "inc/footer.html" .}}
</body>

View File

@ -1,55 +0,0 @@
{{ template "inc/header.html" .}}
<style>
.qrImage[src=""] {
display: none;
}
</style>
<body>
<div class="container is-fullhd p-4 mb-6" style="min-height: 35rem; height: calc(100vh - 18rem)">
<h1 class="title">FreePad</h1>
<img class="qrImage" src="" alt="" style="position: fixed;top: 1rem;left: 1rem;max-width: 13vw;">
<h2 class="subtitle">Reading from <code>{{.domain_base}}/{{.title}}</code></h2>
<hr>
<div class="content">
<div class="block">
<a href="/" class="button is-light">Back Home</a>
<a href="javascript:fetchMyQr()" class="button is-primary">QR</a>
<a href="javascript:updateSelf()" class="button is-success">Save</a>
<p class="mt-3">Status: <code class="has-text-primary" id="status-indicator">Loaded</code></p>
</div>
<textarea class="input" name="post_content" id="post_content" onchange="updateSelf()">{{.post_content}}</textarea>
</div>
</div>
<script>
async function fetchMyQr() {
let qrCode = await getQr(window.location.href)
.catch( err => {
console.error(err);
})
console.log(qrCode);
if ( !!qrCode.qr ) {
document.querySelectorAll(`.qrImage`).forEach( img => {
console.log(img);
console.log(qrCode.qr);
img.src = qrCode.qr;
})
}
}
function updateSelf() {
updatePost({{.title}})
}
</script>
{{ template "inc/footer.html" .}}
</body>
<script src="https://raw.githubusercontent.com/scotch-io/javascript-modal/master/Modal.js"></script>

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

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module github.com/JustKato/FreePad
go 1.15
require (
github.com/gin-gonic/gin v1.7.7
github.com/joho/godotenv v1.4.0
github.com/mrz1836/go-sanitize v1.1.5
)

61
go.sum Normal file
View File

@ -0,0 +1,61 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mrz1836/go-sanitize v1.1.5 h1:LOywG3ijK/B/D9ik3hsniyIzA1JVZlM2wmp3Q/CBk88=
github.com/mrz1836/go-sanitize v1.1.5/go.mod h1:HnnbbJTcBhbr770WyRL4SA95I4FFOnGg/RTLJybsuN8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,36 @@
package controllers
import (
"fmt"
"os"
"strconv"
"time"
"github.com/JustKato/FreePad/lib/objects"
)
func TaskManager() {
// Get the cleanup interval
cleanupIntervalString, exists := os.LookupEnv("CLEANUP_MAX_AGE")
if !exists {
cleanupIntervalString = "-1"
}
if cleanupIntervalString == "-1" {
// Do not cleanup
return
}
// Try and parse the string as an int
cleanupInterval, err := strconv.Atoi(cleanupIntervalString)
if err != nil {
cleanupInterval = 1
}
fmt.Println("[Task::Cleanup]: Task registered")
for range time.Tick(time.Minute * 5) {
objects.CleanupPosts(cleanupInterval)
}
}

151
lib/objects/objects_post.go Normal file
View File

@ -0,0 +1,151 @@
package objects
import (
"fmt"
"os"
"path/filepath"
"time"
)
type Post struct {
Name string `json:"name"`
LastModified string `json:"last_modified"`
Content string `json:"content"`
}
func getStorageDirectory() string {
baseStoragePath, exists := os.LookupEnv("PAD_STORAGE_PATH")
if !exists {
baseStoragePath = "/tmp/"
}
// Check if the base storage path exists
if _, err := os.Stat(baseStoragePath); os.IsNotExist(err) {
// Looks like the base storage path was NOT set, create the dir
err = os.Mkdir(baseStoragePath, 0777)
// Check for errors
if err != nil {
// No way this sends an error unless it goes horribly wrong.
panic(err)
}
}
// Return the base storage path
return baseStoragePath
}
func GetPost(fileName string) Post {
// Get the base storage directory and make sure it exists
storageDir := getStorageDirectory()
// Generate the file path
filePath := fmt.Sprintf("%s%s", storageDir, fileName)
p := Post{
Name: fileName,
Content: "",
LastModified: "Never Before",
}
// Check if the file exits
if _, err := os.Stat(filePath); !os.IsNotExist(err) {
// File does exist, read it and set the content
data, err := os.ReadFile(filePath)
if err != nil {
fmt.Println("Error:", err)
}
// Get the content of the file and put it in the response
p.Content = string(data)
// Get last modified date
fileData, err := os.Stat(filePath)
if err == nil {
p.LastModified = fileData.ModTime().Format("02/01/2006 03:04:05 PM")
} else {
fmt.Println(err)
}
}
return p
}
func WritePost(p Post) error {
// Get the base storage directory and make sure it exists
storageDir := getStorageDirectory()
// Generate the file path
filePath := fmt.Sprintf("%s%s", storageDir, p.Name)
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return err
}
// Write the contnets
_, err = f.WriteString(p.Content)
if err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
return nil
}
func CleanupPosts(age int) {
// Initialize the files buffer
var files []string
// Get the base path
root := getStorageDirectory()
// Scan the directory
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
// Check that the file is not a dir
if !info.IsDir() {
files = append(files, path)
}
return nil
})
// Check if we had any errors
if err != nil {
fmt.Println("[Task::Cleanup]: Error", err)
}
// The timestamp where the file should be deleted
tooOldTime := time.Now()
// Go through all files and process them
for _, filePath := range files {
// Get last modified date
fileData, err := os.Stat(filePath)
if err == nil {
fileAge := fileData.ModTime()
// Check if the file is too old
if fileAge.Add(time.Duration(age)*time.Minute).Unix() < tooOldTime.Unix() {
fmt.Println("[Task::Cleanup]: Removing File", filePath)
// Remove the file
err = os.Remove(filePath)
if err != nil {
fmt.Println("[Task::Cleanup]: Failed to remove file", filePath)
}
}
} else {
fmt.Println("[Task::Cleanup]: Error", err)
}
}
}

78
lib/routes/routes_home.go Normal file
View File

@ -0,0 +1,78 @@
package routes
import (
"net/url"
"time"
"github.com/JustKato/FreePad/lib/helper"
"github.com/JustKato/FreePad/lib/objects"
"github.com/gin-gonic/gin"
"github.com/mrz1836/go-sanitize"
)
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")
// Sanitize the postName
newPostName, err := url.QueryUnescape(postName)
if err == nil {
postName = newPostName
}
postName = sanitize.AlphaNumeric(postName, true)
post := objects.GetPost(postName)
c.HTML(200, "page.html", gin.H{
"title": postName,
"post_content": post.Content,
"last_modified": post.LastModified,
"domain_base": helper.GetDomainBase(),
})
})
router.POST("/:post", func(c *gin.Context) {
// Get the post we are looking for.
postName := c.Param("post")
postContent := c.PostForm("content")
// Sanitize the postName
newPostName, err := url.QueryUnescape(postName)
if err == nil {
postName = newPostName
}
postName = sanitize.AlphaNumeric(postName, true)
p := objects.Post{
Name: postName,
Content: postContent,
LastModified: time.Now().Format("02/01/2006 03:04:05 PM"),
}
// Write the post
err = objects.WritePost(p)
if err != nil {
c.JSON(400, gin.H{
"error": err,
})
// End
return
}
// Return the success message
c.JSON(200, gin.H{
"pad": p,
})
})
}

39
main.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"os"
"github.com/JustKato/FreePad/lib/controllers"
"github.com/JustKato/FreePad/lib/routes"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)
func main() {
// Load environment variables, ignore if any errors come up
godotenv.Load()
dm, isDevelopment := os.LookupEnv("DEV_MODE")
if !isDevelopment && dm == "0" {
gin.SetMode(gin.ReleaseMode)
}
// Run the TaskManager
go controllers.TaskManager()
// 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)
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,12 +0,0 @@
module github.com/JustKato/FreePad
go 1.15
require (
github.com/gin-gonic/gin v1.7.7
github.com/go-sql-driver/mysql v1.6.0
github.com/golang-migrate/migrate/v4 v4.15.2
github.com/mattn/go-sqlite3 v1.14.13
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/ulule/limiter/v3 v3.10.0
)

1886
src/go.sum

File diff suppressed because it is too large Load Diff

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,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"`
}

58
static/css/main.css Normal file
View File

@ -0,0 +1,58 @@
:root {
--color-border-default: #444c56;
--color-fg-default: #adbac7;
}
.light body {
background-color: whitesmoke;
}
main#main-card {
max-width: 768px;
}
.light main#main-card {
background-color: white;
}
.dark main#main-card {
background-color: var(--bs-body-bg-alt);
border: 1px solid var(--color-border-default);
}
.dark {
color: var(--color-fg-default);
}
.dark .hidedark, .light .hidelight {
display: none !important;
}
#theme-toggle {
position: fixed;
top: .5rem;
right: .5rem;
}
textarea:focus,
input[type="text"]:focus,
input[type="password"]:focus,
input[type="datetime"]:focus,
input[type="datetime-local"]:focus,
input[type="date"]:focus,
input[type="month"]:focus,
input[type="time"]:focus,
input[type="week"]:focus,
input[type="number"]:focus,
input[type="email"]:focus,
input[type="url"]:focus,
input[type="search"]:focus,
input[type="tel"]:focus,
input[type="color"]:focus,
.uneditable-input:focus {
border-color: none;
box-shadow: none;
outline: 0 none;
}

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

View File

@ -0,0 +1,4 @@
{{ define "inc/footer.html"}}
<script src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"></script>
<script src="/static/js/main.js"></script>
{{ end }}

21
templates/inc/header.html Normal file
View File

@ -0,0 +1,21 @@
{{ define "inc/header.html"}}
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FreePad - {{.title}}</title>
<meta property="og:title" content="FreePad.com" />
<meta property="og:type" content="article" />
<meta property="og:site_name" content="FreePad" />
<meta property="og:url" content="{{.domain_base}}/" />
<link rel="icon" type="image/png" href="/static/img/favicon.png"/>
<meta name="color-scheme" content="light dark">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" rel="stylesheet">
<!-- Love https://vinorodrigues.github.io/bootstrap-dark-5/ -->
<link rel="stylesheet" href="/static/css/main.css">
</head>
{{ end }}

View File

@ -0,0 +1,11 @@
{{ define "inc/theme-toggle.html"}}
<button type="button" class="btn btn-white" id="theme-toggle" onclick="darkmode.toggleDarkMode();">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-sun hidelight" viewBox="0 0 16 16">
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-moon-stars hidedark" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z"/>
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
</svg>
</button>
{{ end }}

View File

@ -0,0 +1,64 @@
{{ template "inc/header.html" .}}
<style>
</style>
<body>
<main id="main-card" class="container rounded mt-5 shadow-sm">
<div class="p-3">
<div class="logo-container w-100 d-flex mb-4">
<img src="/static/img/logo_transparent.png" alt="Logo" style="max-width: 50%; margin: 0 auto;" class="mx-auto">
</div>
<div class="form-group my-4">
<form class="search-action input-group" onsubmit="goToPad(); return false;">
<input type="text" class="form-control form-control-lg" placeholder="What's your pad?" aria-label="What's your pad?" aria-describedby="pad-name-button" id="pad-name">
<button class="btn btn-primary" type="submit" id="pad-name-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-box-arrow-in-down" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.5 6a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-2a.5.5 0 0 1 0-1h2A1.5 1.5 0 0 1 14 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 14.5v-8A1.5 1.5 0 0 1 3.5 5h2a.5.5 0 0 1 0 1h-2z" />
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z" />
</svg>
</button>
</form>
</div>
<script>
function goToPad() {
// Go to the next apd
window.location.href = "/" + document.getElementById(`pad-name`).value;
}
</script>
<div class="why mb-4">
<p>
Ever wanted to transport information across platforms
without the hassle of loading times, sending files, logins, etc?
</p>
<p>
<b>FreePad</b> can easily store text information in any predefined url,
just write it in the box above and get to the right page, write anything in
and access the same address on any other device to get your info!
</p>
</div>
</div>
<footer class="text-muted py-5 border-top text-center">
<p class="mb-1">
FreePad by <a href="https://justkato.me/">©Kato Twofold</a>
</p>
<p class="mb-0">
FreePad is freely available over on our <a href="https://github.com/JustKato/FreePad">GitHub</a>
</p>
</footer>
</main>
{{ template "inc/theme-toggle.html" .}}
</body>
{{ template "inc/footer.html" .}}

127
templates/pages/page.html Normal file
View File

@ -0,0 +1,127 @@
{{ template "inc/header.html" .}}
<style>
#pad-content {
height: 16rem;
}
</style>
<body>
<main id="main-card" class="container rounded mt-5 shadow-sm">
<div class="p-3">
<div class="logo-container w-100 d-flex mb-4">
<img src="/static/img/logo_transparent.png" alt="Logo" style="max-width: 50%; margin: 0 auto;" class="mx-auto">
</div>
</div>
<textarea name="pad-content" id="pad-content" onchange="sendMyData(this)" onkeydown="updateStatus(`Not Saved`, `text-danger`)" class="form-control">{{.post_content}}</textarea>
<div id="pad-status" class="my-4 row">
<div class="col-md-12 col-lg-4 col-xl-4" title="Status">
<div class="input-group">
<span class="input-group-text">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-3" viewBox="0 0 16 16">
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-8zm4 8a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
</svg>
</span>
<input type="text" class="form-control" readonly value="Loaded" id="loading_status">
</div>
</div>
<div class="col-md-12 col-lg-4 col-xl-4 mt-4 mt-lg-0 mt-xl-0" title="Current Viewers">
<div class="input-group">
<span class="input-group-text">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"></path>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"></path>
</svg>
</span>
<input type="text" class="form-control" readonly value="1">
</div>
</div>
<div class="col-md-12 col-lg-4 col-xl-4 mt-4 mt-lg-0 mt-xl-0" title="Last Modified">
<div class="input-group">
<span class="input-group-text">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hourglass-split" viewBox="0 0 16 16">
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
</svg>
</span>
<input type="text" class="form-control" id="last_modified_" readonly value="{{.last_modified}}">
</div>
</div>
</div>
<footer class="text-muted py-5 text-center">
<p class="mb-1">
FreePad by <a href="https://justkato.me/">©Kato Twofold</a>
</p>
<p class="mb-0">
FreePad is freely available over on our <a href="https://github.com/JustKato/FreePad">GitHub</a>
</p>
</footer>
</main>
{{ template "inc/theme-toggle.html" .}}
</body>
<script>
function sendMyData(el) {
const formData = new FormData();
el.setAttribute(`readonly`, `1`);
formData.set("content", el.value);
updateStatus(`Attempting to save...`, `text-warning`);
fetch(window.location.href.toString(), {
body: formData,
method: "post",
})
.then( resp => {
console.log(resp);
resp.json()
.then( e => {
console.log(e.pad);
document.getElementById(`last_modified_`).value = e.pad.last_modified;
updateStatus(`Succesfully Saved`, `text-success`);
})
.catch( err => {
console.error(err);
updateStatus(`Failed to Save`, `text-error`);
})
})
.catch( err => {
console.error(err);
updateStatus(`Failed to Save`, `text-error`);
})
.finally( () => {
el.removeAttribute(`readonly`);
})
}
function updateStatus(txt, cls) {
const loading_status = document.getElementById(`loading_status`)
loading_status.value = txt;
loading_status.classList.remove("text-danger", "text-warning", "text-success", "text-white", "text-primary");
loading_status.classList.add(cls);
}
document.addEventListener(`DOMContentLoaded`, e => {
document.getElementById(`pad-content`).focus();
})
</script>
{{ template "inc/footer.html" .}}