first commit

This commit is contained in:
sML 2022-12-27 10:00:40 +01:00
commit 84948f84c4
9 changed files with 690 additions and 0 deletions

67
README.md Normal file
View File

@ -0,0 +1,67 @@
# Intro
afr0dita is a web server where you can download and organize your images by tag.
You only need to put an URL with the image that you want to download, add tags and then afr0dita will download
and save the image associated with the tag, after that you can search images by tag.
Check changelog.txt to see the current status.
# How it looks.
![afr0dita-image](afr0dita.png)
# To know..
Feel free to change the code in order to adapt the program to your needs.
During the first run the program:
* Creates ~/afr0dita.db file which is the database.
* Creates ~/.afr0ditafiles which is the folder to store the images.
* Creates a link from ~/.afr0ditafiles to screenshots/ in the same folder where is the executable.
* The server uses the port 8001.
Check that you have permissions to run the program in the port 8001 and also write permissions in screenshots folder in order to
download the screenshots if needed.
# Installation
```sh
git clone https://code.lacashita.com/sml/afr0dita
go build afr0dita.go
./afr0dita
```
Once started, you can access at http://127.0.0.1:8001
# Upgrade
Just clone the repo with the last version, compile and replace with the latest binary.
# Install as a service.
Create a user to manage the service.
```sh
/usr/sbin/adduser --system --shell /bin/bash --gecos 'afr0dita Server' --group --disabled-password --home /home/afrodita afrodita
```
Create file /etc/systemd/system/afr0dita.service
```sh
[Unit]
Description=afr0dita Server
After=syslog.target
After=network.target
[Service]
RestartSec=2s
Type=simple
User=afrodita
Group=afrodita
ExecStart=/home/afrodita/afr0dita
Restart=always
Environment=USER=afrodita HOME=/home/afrodita
[Install]
WantedBy=multi-user.target
```
Finally, enable and start the service.
```sh
sudo systemctl enable afrd0dita
sudo systemctl start afr0dita
```

382
afr0dita.go Executable file
View File

@ -0,0 +1,382 @@
/*
Description: Tool to save images.
Auth0r: sml@lacashita.com
Use it only as educational purpose.
*/
package main
import (
"database/sql"
"fmt"
"html/template"
"log"
"net/http"
"io"
"errors"
"io/ioutil"
"os"
"os/exec"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
"crypto/md5"
"encoding/hex"
)
// Start Web PART
type urlinfo struct {
Date string
Url string
Tags string
Screenshot string
Separated []string
}
type Message struct {
Msj string
}
func main() {
var filedb string
var homepath = os.Getenv("HOME")
if os.Getenv("afr0dita") == "" {
filedb = homepath+"/afr0dita.db"
} else {
filedb = os.Getenv("afr0dita")
}
// Create ~/.afr0ditafiles folder to save screenshots
if _, err := os.Stat(homepath+"/.afr0ditafiles"); os.IsNotExist(err) {
err = os.Mkdir(homepath+"/.afr0ditafiles", 0755)
if err != nil {
log.Fatal(err)
}
}
// Link ~/.afr0ditafiles to screenshots/ folder.
if _, err := os.Stat("screenshots"); os.IsNotExist(err) {
cmd := exec.Command("/usr/bin/ln", "-s", homepath+"/.afr0ditafiles", "screenshots")
_, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
}
checkDB(filedb)
sqliteDatabase, _ := sql.Open("sqlite3", filedb)
defer sqliteDatabase.Close()
startServer(sqliteDatabase)
}
// Func to check if DB exists, if not create it.
func checkDB(filedb string) {
_, err := os.Stat(filedb)
//If file afr0dita.db doesnt exist.
if err != nil {
fmt.Println("[+]Creating Database\n")
file, err := os.Create(filedb)
if err != nil {
log.Fatal(err.Error())
}
file.Close()
sqlDB, _ := sql.Open("sqlite3",filedb)
defer sqlDB.Close()
createTable(sqlDB)
}
}
// Func to create DB.
func createTable(db *sql.DB) {
createafr0ditatable := `CREATE TABLE afr0dita (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"date" TEXT,
"url" TEXT,
"tags" TEXT,
"screenshot" TEXT
);`
statement, err := db.Prepare(createafr0ditatable)
if err != nil {
log.Fatal(err.Error())
}
statement.Exec()
log.Println("[+] afr0dita table created")
}
func startServer(db *sql.DB) {
// All requests to /screenshots/ will serve the static content of screenshots/
fs := http.FileServer(http.Dir("./screenshots"))
http.Handle("/screenshots/", http.StripPrefix("/screenshots/", fs))
http.HandleFunc("/", afr0ditaHandler(db))
// All requests to /styles/css will serve static/css/bootstrap.min.css.
http.HandleFunc("/styles/css", func(response http.ResponseWriter, request *http.Request) {
http.ServeFile(response, request, "static/css/bootstrap.min.css")
})
http.HandleFunc("/add", func(response http.ResponseWriter, request *http.Request) {
http.ServeFile(response, request, "static/html/add.html")
})
http.ListenAndServe("127.0.0.1:8001", nil)
}
func afr0ditaHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var repetido int
tagtose := r.URL.Query()["tags"]
tagtosearch := strings.Join(tagtose ," ")
urltoa := r.URL.Query()["url"]
urltoadd := strings.Join(urltoa, " ")
repetido = dupURL(db,urltoadd)
urltodel := r.URL.Query()["delete"]
urltodelete := strings.Join(urltodel ," ")
var homepath = os.Getenv("HOME")
filename := getFilename(urltoadd)
screenshotname := filename+".jpg"
screenshotfullpath := homepath+"/.afr0ditafiles/"+filename+".jpg"
if urltodelete != "" {
imageURLname := getImageName(db,urltodelete)
filetodelete := "screenshots/"+imageURLname
os.Remove(filetodelete)
deleteUrl(db,urltodelete)
}
if urltoadd == "" {
displayafr0dita(db, w,tagtosearch)
} else {
if repetido != 0 {
m := Message{"URL is duplicated."}
printMessage(w,m)
} else {
err := archiveURL(urltoadd,screenshotfullpath,w)
if err != nil {
m := Message{"Error with the image."}
printMessage(w,m)
} else {
insertUrl(db,urltoadd,tagtosearch,screenshotname)
m := Message{"URL added successfully."}
printMessage(w,m)
}
}
}
}
}
func displayafr0dita(db *sql.DB, w http.ResponseWriter,tagtosearch string) {
var Arrayurlinfo []urlinfo
var Repetidos []string
tmpl := template.Must(template.ParseFiles("static/html/main.html"))
p := urlinfo{}
if tagtosearch == "" {
row, err := db.Query("SELECT date,url,tags,screenshot FROM afr0dita")
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var date string
var url string
var tags string
var screenshot string
row.Scan( &date, &url, &tags, &screenshot)
// s is []string where tags are separated by ,
s := strings.Split(tags, ",")
//All s elementes are appended to Repetidos []string
for _,element := range s {
Repetidos = append(Repetidos,element)
}
p = urlinfo{date,url,tags,screenshot,s}
Arrayurlinfo = append(Arrayurlinfo, p)
}
}
if tagtosearch != "" {
row, err := db.Query("SELECT date,url,tags,screenshot FROM afr0dita where tags like ?","%"+tagtosearch+"%")
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var date string
var url string
var tags string
var screenshot string
row.Scan( &date, &url, &tags, &screenshot)
var s []string
s = strings.Split(tags, ",")
for _,element := range s {
Repetidos = append(Repetidos,element)
}
p = urlinfo{date,url,tags,screenshot,s}
Arrayurlinfo = append(Arrayurlinfo, p)
}
}
//Repetidos store now []string without tags duplicated.
Repetidos = removeDuplicates(Repetidos)
// Creates the last urlinfo struct with Repetidos.
lastp := urlinfo{"","","","",nil}
Arrayurlinfo = append(Arrayurlinfo,lastp)
tmpl.Execute(w,Arrayurlinfo)
}
// Func to Print Messages through Web.
func printMessage(w http.ResponseWriter, msg Message) {
tmpl := template.Must(template.ParseFiles("static/html/message.html"))
tmpl.Execute(w, msg)
}
// Inserting URL into SQL.
func insertUrl(db *sql.DB, url string, tagstoinsert string, screenshotname string) {
currentTime := time.Now()
date := currentTime.Format("01-02-2006")
insertURLSQL := `INSERT INTO afr0dita(date,url,tags,screenshot) VALUES (?,?,?,?)`
statement, err := db.Prepare(insertURLSQL)
if err != nil {
log.Fatalln(err.Error())
}
_, err = statement.Exec(date, url, tagstoinsert,screenshotname)
if err != nil {
log.Fatalln(err.Error())
}
}
// Delete URL.
func deleteUrl(db *sql.DB, url string) {
deleteURLSQL := `DELETE FROM afr0dita WHERE url = ?`
statement, err := db.Prepare(deleteURLSQL)
if err != nil {
log.Fatalln(err.Error())
}
_, err = statement.Exec(url)
if err != nil {
log.Fatalln(err.Error())
}
}
// Func to update an URL to add the filename of the screenshot.
func updateUrl(db *sql.DB, filename string, url string) {
updateURLSQL := `UPDATE afr0dita set screenshot = ? WHERE url = ?`
statement, err := db.Prepare(updateURLSQL)
if err != nil {
log.Fatalln(err.Error())
}
_, err = statement.Exec(filename,url)
if err != nil {
log.Fatalln(err.Error())
}
}
// Func to check if URL is duplicated, it returns an int with the number of times that URL is repeated.
func dupURL(db *sql.DB, url string) int{
var total int
row, err := db.Query("SELECT count(*) as total from afr0dita where url = ?",url)
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var count int
row.Scan( &count)
total = count
}
return total
}
// Func to get the "id" that will be used as imagename from an given URL, it returns the id.
func getImageName(db *sql.DB, url string) string{
var imagename string
row, err := db.Query("SELECT screenshot from afr0dita where url = ?",url)
if err != nil {
log.Fatal(err)
}
defer row.Close()
for row.Next() {
var screenshot string
row.Scan( &screenshot)
imagename = screenshot
}
return imagename
}
// Func to remove duplicated strings from an slice.
func removeDuplicates(strSlice []string) []string {
allKeys := make(map[string]bool)
list := []string{}
for _, item := range strSlice {
if _, value := allKeys[item]; !value {
allKeys[item] = true
list = append(list, item)
}
}
return list
}
// Func to create a filename using the md5 hash of the url name.
func getFilename(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}
// Func to take one screenshot from the given URL and save it to a file. It returns error.
func archiveURL(URL string, fileName string,w http.ResponseWriter) error {
// Set http client to make the request with custom User-Agent.
client := &http.Client{}
req, _ := http.NewRequest("GET", URL, nil)
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0")
// Makes request.
response, err := client.Do(req)
if err != nil {
return err
}
defer response.Body.Close()
// Check the URL and if the mime is an image or not. If not return an error.
bytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
mimeType := http.DetectContentType(bytes)
if mimeType != "image/jpg" && mimeType != "image/jpg" && mimeType != "image/png" && mimeType != "image/bmp" && mimeType != "image/jpeg" {
m := Message{"URL is not an image."}
printMessage(w,m)
err := errors.New("URL is not an image")
return err
}
// If mime is an image makes a request to get the image.
response, err = client.Do(req)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != 200 {
m := Message{"Returning not 200 Code"}
printMessage(w,m)
}
//Create a empty file
file, err := os.Create(fileName)
if err != nil {
m := Message{"Error creating the image."}
printMessage(w,m)
return err
}
defer file.Close()
//Write the bytes to the file
_, err = io.Copy(file, response.Body)
if err != nil {
m := Message{"Error copying the image."}
printMessage(w,m)
return err
}
return nil
}

BIN
afr0dita.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

19
go.mod Normal file
View File

@ -0,0 +1,19 @@
module afrodita
go 1.19
require (
github.com/chromedp/chromedp v0.8.6
github.com/mattn/go-sqlite3 v1.14.16
)
require (
github.com/chromedp/cdproto v0.0.0-20220924210414-0e3390be1777 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
)

23
go.sum Normal file
View File

@ -0,0 +1,23 @@
github.com/chromedp/cdproto v0.0.0-20220924210414-0e3390be1777 h1:nEnjcdmVQjhtQm0RFJxRINMw7lsQ8gidtbpsidiDqpY=
github.com/chromedp/cdproto v0.0.0-20220924210414-0e3390be1777/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0=
github.com/chromedp/chromedp v0.8.6 h1:KobeeqR2dpfKSG1prS3Y6+FbffMmGC6xmAobRXA9QEQ=
github.com/chromedp/chromedp v0.8.6/go.mod h1:nBYHoD6YSNzrr82cIeuOzhw1Jo/s2o0QQ+ifTeoCZ+c=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

7
static/css/bootstrap.min.css vendored Executable file

File diff suppressed because one or more lines are too long

61
static/html/add.html Executable file
View File

@ -0,0 +1,61 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="styles/css">
<title>afr0dita</title>
<style>
body {
background-color: #FFFDFA;
}
input[type="text"], textarea {
background-color : #FFFDFA;
}
.form-control-sm {
font-family: inherit;
width: 100%;
border: 0;
border-bottom: 2px solid black;
outline: 0;
font-size: 1.3rem;
padding: 7px 0;
background: transparent;
transition: border-color 0.2s;
}
</style>
</head>
<body>
<div class="container">
<div class="d-flex flex-column min-vh-100 justify-content-center align-items-center">
<h1 class="py-5">afr0dita</h1>
<form action="/">
<div class="row">
<div class="col-7">
<input class="form-control-sm" type="text" autocomplete="off" id="url" name="url" placeholder="URL"><br><br>
</div>
<div class="col-5">
<input class="form-control-sm" type="text" autocomplete="off" id="tags" name="tags" placeholder="tags"><br><br>
</div>
</div>
<div class="row py-3">
<div class="py-3">
<input type="submit" class="btn btn-outline-dark" value="Submit">
</div>
</div>
</form>
</div>
</div>
</body>
</html>

100
static/html/main.html Executable file
View File

@ -0,0 +1,100 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="styles/css">
<title>afr0dita</title>
<style>
body {
background-color: #FFFDFA;
padding-top: 100px;
font-family: "Ubuntu Mono", monospace;
}
input[type="text"] {
background-color : #FFFDFA;
}
.fixed-top {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 1030;
}
.card-group {
margin-right:5%;
}
.form-control-sm {
font-family: inherit;
width: 100%;
border: 0;
border-bottom: 2px solid black;
outline: 0;
font-size: 1.3rem;
padding: 7px 0;
background: transparent;
transition: border-color 0.2s;
}
</style>
</head>
<body>
<nav class="navbar navbar-light fixed-top navbar-masthead">
<div class="container-fluid">
<a class="navbar-brand">
<form action="/add">
<input type="submit" class="btn btn-outline-dark" value="+ Add" />
</form>
</a>
<h1>afr0dita</h1>
</div>
</nav>
<div class="d-flex align-items-center">
<div class="container py-5">
<div class="row">
<div class="col">
<form class="d-flex" action="/">
<input class="form-control-sm me-2" autocomplete="off" type="text" name="tags" id="tags" placeholder="Search" aria-label="Search">
</form>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-center">
<div class="container">
<div class="row">
{{range .}}
{{ if eq .Screenshot "" }}
{{ else }}
<div class="col-3 py-2">
<a target="_blank" href="screenshots/{{.Screenshot}}"><img class="card" src="screenshots/{{.Screenshot}}" width="300" height="200" ></a>
{{range .Separated}}
<span class="badge bg-light"><a href="?tags={{.}}">{{.}}</a></span>
{{end}}
{{ if eq .Url "" }}
{{ else }}
<a href="/?delete={{.Url}}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg></a>
{{end}}
</div>
{{ end }}
{{end}}
</div>
</div>
</div>
</body>
</html>

31
static/html/message.html Executable file
View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="styles/css">
<title>afr0dita Message</title>
<style>
body {
background-color: #FFFDFA;
}
</style>
</head>
<body>
<div class="d-flex align-items-center hv-100 vh-100">
<div class="container text-center">
<div>{{.Msj}}</div>
<a class="my-3 badge rounded-pill bg-dark" href="/">Back</a>
</div>
</div>
</body>
</html>