Browse Source

Initial commit

Piotr Czajkowski 1 year ago
commit
2eda4a2ca2
5 changed files with 274 additions and 0 deletions
  1. 12 0
      hash.go
  2. 35 0
      html/index.html
  3. 38 0
      html/result.html
  4. 96 0
      server.go
  5. 93 0
      storage.go

+ 12 - 0
hash.go

@@ -0,0 +1,12 @@
+package main
+
+import (
+	"fmt"
+	"hash/crc32"
+)
+
+var crc32q *crc32.Table = crc32.MakeTable(0xD5828281)
+
+func getHash(link string) string {
+	return fmt.Sprintf("%08x", crc32.Checksum([]byte(link), crc32q))
+}

+ 35 - 0
html/index.html

@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+	<head>
+		<meta charset="utf-8">
+		<script async src="https://cdn.ampproject.org/v0.js"></script>
+		<title>Shorty - shorten URL</title>
+		<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
+		<script type="application/ld+json">
+			{
+				"@context": "http://schema.org",
+				"@type": "NewsArticle",
+				"headline": "Open-source framework for publishing content",
+				"datePublished": "2015-10-07T12:02:41Z",
+				"image": [
+				"logo.jpg"
+				]
+			}
+		</script>
+		<link rel="stylesheet" href="https://newcss.net/lite.css">
+		<link rel="stylesheet" href="https://newcss.net/theme/night.css">
+	</head>
+	<body>
+		<form action="/s">
+			Link to shorten:
+			<input type="text" name="link">
+			<input type="submit" value="Shorten">
+		</form>
+		<br/>
+		<form action="/d">
+			Link to decode:
+			<input type="text" name="link">
+			<input type="submit" value="Decode">
+		</form>
+	</body>
+</html>

+ 38 - 0
html/result.html

@@ -0,0 +1,38 @@
+<!doctype html>
+<html>
+	<head>
+		<meta charset="utf-8">
+		<script async src="https://cdn.ampproject.org/v0.js"></script>
+		<title>Your link</title>
+		<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
+		<script type="application/ld+json">
+			{
+				"@context": "http://schema.org",
+				"@type": "NewsArticle",
+				"headline": "Open-source framework for publishing content",
+				"datePublished": "2015-10-07T12:02:41Z",
+				"image": [
+				"logo.jpg"
+				]
+			}
+		</script>
+		<link rel="stylesheet" href="https://newcss.net/lite.css">
+		<link rel="stylesheet" href="https://newcss.net/theme/night.css">
+		<script>
+			function copyLink() {
+  				const copyText = document.getElementById("link");
+  				copyText.select();
+  				document.execCommand("copy");
+
+				let info = document.getElementById("info");
+				info.textContent = "Copied!";
+}
+		</script>
+
+	</head>
+	<body>
+		<input type="text" value="{{.}}" id="link"/>
+		<button onclick="copyLink()">Copy link</button>
+		<div id="info"></div>
+	</body>
+</html>

+ 96 - 0
server.go

@@ -0,0 +1,96 @@
+package main
+
+import (
+	"flag"
+	"html/template"
+	"log"
+	"net/http"
+	"strings"
+)
+
+const (
+	shortenPath = "/s/"
+	decodePath  = "/d/"
+)
+
+var host = flag.String("h", "localhost", "Host on which to serve")
+var port = flag.String("p", "9090", "Port on which to serve")
+var file = flag.String("f", "links.txt", "File to which save links")
+var domain = flag.String("d", "", "Domain of shorty, preferably add schema")
+
+var toSave chan string
+
+func init() {
+	toSave = make(chan string, 100)
+}
+
+func shorten(w http.ResponseWriter, r *http.Request) {
+	link := r.URL.Query().Get("link")
+	if link == "" {
+		link = strings.TrimPrefix(r.URL.Path, shortenPath)
+	}
+
+	linkID := addLink(link, toSave)
+
+	t := template.Must(template.ParseFiles("./html/result.html"))
+
+	shortened := r.Host + "/" + linkID
+	if *domain != "" {
+		shortened = *domain + "/" + linkID
+	}
+	t.Execute(w, shortened)
+}
+
+func decode(w http.ResponseWriter, r *http.Request) {
+	link := r.URL.Query().Get("link")
+	if link == "" {
+		link = strings.TrimPrefix(r.URL.Path, shortenPath)
+	}
+
+	t := template.Must(template.ParseFiles("./html/result.html"))
+
+	parts := strings.Split(link, "/")
+	linkID := parts[len(parts)-1]
+	if linkID != "" {
+		fullLink := getLink(linkID)
+		if fullLink != "" {
+			t.Execute(w, fullLink)
+			return
+		}
+	}
+
+	t.Execute(w, "Not found!")
+}
+
+func serveIndex(w http.ResponseWriter, r *http.Request) {
+	t := template.Must(template.ParseFiles("./html/index.html"))
+	t.Execute(w, nil)
+}
+
+func redirectOrServe(w http.ResponseWriter, r *http.Request) {
+	linkID := strings.TrimPrefix(r.URL.Path, "/")
+
+	if linkID == "" {
+		serveIndex(w, r)
+	} else {
+		link := getLink(linkID)
+		if link != "" {
+			http.Redirect(w, r, link, http.StatusMovedPermanently)
+		} else {
+			serveIndex(w, r)
+		}
+	}
+}
+
+func main() {
+	flag.Parse()
+	readLinks(*file)
+
+	go saveLink(*file, toSave)
+
+	hostname := *host + ":" + *port
+	http.HandleFunc(shortenPath, shorten)
+	http.HandleFunc(decodePath, decode)
+	http.HandleFunc("/", redirectOrServe)
+	log.Fatal(http.ListenAndServe(hostname, nil))
+}

+ 93 - 0
storage.go

@@ -0,0 +1,93 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"log"
+	"os"
+	"strings"
+	"sync"
+)
+
+const (
+	format = "%s<>%s\n"
+)
+
+var links sync.Map
+
+func readLinks(path string) {
+	file, err := os.OpenFile(path, os.O_CREATE|os.O_RDONLY, 0644)
+	if err != nil {
+		log.Fatalf("Failed to open %s!\n", path)
+
+	}
+
+	defer func() {
+		if err := file.Close(); err != nil {
+			log.Fatalf("Failed to close file: %s", err)
+		}
+	}()
+
+	scanner := bufio.NewScanner(file)
+
+	for scanner.Scan() {
+		line := scanner.Text()
+		if line == "" {
+			break
+		}
+
+		parts := strings.Split(line, "<>")
+		if len(parts) != 2 {
+			log.Fatalf("Wrong line format: %s", line)
+		}
+
+		links.Store(parts[0], parts[1])
+	}
+	if err := scanner.Err(); err != nil {
+		log.Fatalf("Scanner error: %s", err)
+	}
+
+}
+
+func addLink(link string, toSave chan<- string) string {
+	linkID := getHash(link)
+
+	existingLink, loaded := links.LoadOrStore(linkID, link)
+	if loaded {
+		if existingLink != link {
+			log.Printf("Have collision:\n%s\n%s\n", link, existingLink)
+		}
+	} else {
+		toSave <- fmt.Sprintf(format, linkID, link)
+	}
+
+	return linkID
+}
+
+func getLink(linkID string) string {
+	link, found := links.Load(linkID)
+	if !found {
+		return ""
+	}
+
+	return link.(string)
+}
+
+func saveLink(path string, toSave <-chan string) {
+	file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	defer func() {
+		if err := file.Close(); err != nil {
+			log.Fatalf("Failed to close file: %s", err)
+		}
+	}()
+
+	for item := range toSave {
+		if _, err := file.WriteString(item); err != nil {
+			log.Fatal(err)
+		}
+	}
+}