|
@ -1,5 +0,0 @@
|
|||
dev
|
||||
.gitignore
|
||||
LICENSE
|
||||
README.md
|
||||
dist/main.db
|
19
.env.example
|
@ -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
|
|
@ -2,7 +2,4 @@ dev/*
|
|||
!dev/.keep
|
||||
.env
|
||||
docker-compose.yaml
|
||||
# Ignore the database file
|
||||
dist/main.db
|
||||
dist/freepad
|
||||
dist/freepad
|
||||
data
|
15
Dockerfile
|
@ -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"]
|
80
README.md
|
@ -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!
|
15
build.sh
|
@ -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;
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE IF EXISTS t_posts;
|
|
@ -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
|
||||
;
|
|
@ -1 +0,0 @@
|
|||
DROP table t_posts;
|
|
@ -1,6 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS "t_posts" (
|
||||
"id" INTEGER,
|
||||
"name" TEXT,
|
||||
"content" TEXT,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
|
@ -1,3 +0,0 @@
|
|||
#post_content {
|
||||
height: calc(100vh - 35rem);
|
||||
}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
}
|
|
@ -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")
|
||||
|
||||
}
|
23
rundev.sh
|
@ -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;
|
|
@ -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
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package post
|
||||
|
||||
type Post struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
12
src/go.mod
|
@ -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
|
@ -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)
|
||||
}
|
45
src/main.go
|
@ -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")
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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",
|
||||
})
|
||||
}
|
|
@ -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(),
|
||||
})
|
||||
})
|
||||
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package types
|
||||
|
||||
type FreeError struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
|
@ -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;
|
||||
}
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 218 B |
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 780 B After Width: | Height: | Size: 780 B |
Before Width: | Height: | Size: 1021 B After Width: | Height: | Size: 1021 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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" .}}
|
|
@ -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" .}}
|