website.git

commit 4f0b1ed1c0ae49e3d0c4726089dce734d6ada7e6

Author: Adam Evyčędo <git@apiote.xyz>

check duplicates in link shorts

 router.go | 145 ++++++++++++++++++++++++++++++++++----------------------


diff --git a/router.go b/router.go
index d90ba8d040ec0ea9b3c7cd176dbb298bd1984bda..740814420b731873128426f2d91c86b250bf0cdd 100644
--- a/router.go
+++ b/router.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bufio"
 	"bytes"
 	"embed"
 	"fmt"
@@ -156,15 +157,17 @@ 	t, err := template.ParseFS(templatesFS, "templates/"+name+".html", "templates/aside.html", "templates/header.html", "templates/nav.html", "templates/head.html", "templates/head_program.html")
 	if err != nil {
 		log.Println(err)
 		if os.IsNotExist(err) {
-			renderStatus(w, 501, acceptLanguage)
+			renderStatus(w, http.StatusNotImplemented, acceptLanguage)
 			return
 		}
+		w.WriteHeader(http.StatusInternalServerError)
 		fmt.Fprint(w, "Error 500")
 		return
 	}
 	err = t.Execute(w, data)
 	if err != nil {
 		log.Println(err)
+		w.WriteHeader(http.StatusInternalServerError)
 		fmt.Fprint(w, "Error 500")
 		return
 	}
@@ -182,7 +185,7 @@ 	if path[0] == "" {
 		showHtml(w, "index", nil, acceptLanguage)
 	}
 	if path[0] != "" {
-		renderStatus(w, 404, acceptLanguage)
+		renderStatus(w, http.StatusNotFound, acceptLanguage)
 	}
 }
 
@@ -216,7 +219,7 @@ 	acceptLanguage := r.Header.Get("Accept-Language")
 	path := strings.Split(r.URL.Path[1:], "/")
 	programs, err := readPrograms()
 	if err != nil {
-		renderStatus(w, 500, acceptLanguage)
+		renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 		return
 	}
 	var (
@@ -236,17 +239,17 @@ 		name := path[1]
 		program = programs[name]
 		version, present = program.Versions[path[2]]
 	} else {
-		renderStatus(w, 404, acceptLanguage)
+		renderStatus(w, http.StatusNotFound, acceptLanguage)
 		return
 	}
 	if !present {
-		renderStatus(w, 404, acceptLanguage)
+		renderStatus(w, http.StatusNotFound, acceptLanguage)
 		return
 	}
 	file, err := programsFS.Open("programs/" + version.Description)
 	if err != nil {
 		if os.IsNotExist(err) {
-			renderStatus(w, 404, acceptLanguage)
+			renderStatus(w, http.StatusNotFound, acceptLanguage)
 			return
 		} else {
 			log.Println(err)
@@ -283,22 +286,22 @@ 	acceptLanguage := r.Header.Get("Accept-Language")
 	accept := r.Header["Accept"][0]
 	path := strings.Split(r.URL.Path[1:], "/")
 	if path[1] == "" {
-		renderStatus(w, 404, acceptLanguage)
+		renderStatus(w, http.StatusNotFound, acceptLanguage)
 	} else {
 		file, err := staticFS.Open("static/" + path[1] + ".asc")
 		defer file.Close()
 		if err != nil && os.IsNotExist(err) {
-			renderStatus(w, 404, acceptLanguage)
+			renderStatus(w, http.StatusNotFound, acceptLanguage)
 			return
 		} else if err != nil {
 			log.Println(err)
-			fmt.Fprint(w, "Error 500")
+			renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 			return
 		}
 		key, err := ioutil.ReadAll(file)
 		if err != nil {
 			log.Println(err)
-			fmt.Fprint(w, "Error 500")
+			renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 			return
 		}
 		w.Header().Set("Vary", "Accept")
@@ -418,11 +421,11 @@ 		acceptLanguage := r.Header.Get("Accept-Language")
 		file, err := os.Open(filepath.Join(dataDir, "holidays.dirty"))
 		if err != nil {
 			if os.IsNotExist(err) {
-				renderStatus(w, 404, acceptLanguage)
+				renderStatus(w, http.StatusNotFound, acceptLanguage)
 				return
 			} else {
 				log.Println(err)
-				fmt.Fprint(w, "Error 500")
+				renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 				return
 			}
 		}
@@ -432,17 +435,17 @@ 		err = dirty.LoadStruct(file, &holidays)
 		if err != nil {
 			log.Println(err)
 			log.Println(holidays)
-			fmt.Fprint(w, "Error 500")
+			renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 			return
 		}
 		t, err := template.ParseFS(templatesFS, "templates/calendar.ics")
 		if err != nil {
 			log.Println(err)
 			if os.IsNotExist(err) {
-				renderStatus(w, 501, acceptLanguage)
+				renderStatus(w, http.StatusNotImplemented, acceptLanguage)
 				return
 			}
-			fmt.Fprint(w, "Error 500")
+			renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 			return
 		}
 		thisYear := time.Now().Year()
@@ -453,7 +456,7 @@ 		w.Header().Set("Content-Type", "text/calendar")
 		err = t.Execute(w, holidays)
 		if err != nil {
 			log.Println(err)
-			fmt.Fprint(w, "Error 500")
+			renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 			return
 		}
 	}
@@ -482,68 +485,97 @@ 				} else {
 					id = strings.ToUpper(id)
 				}
 				if url == "" {
-					renderStatus(w, 400, acceptLanguage)
+					renderStatus(w, http.StatusBadRequest, acceptLanguage)
 					return
 				}
 				hash, err := ioutil.ReadFile(filepath.Join(stateDir, "password"))
 				if err != nil {
 					log.Println(err)
-					fmt.Fprint(w, "Error 500")
+					renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 					return
 				}
 				passMatch, err := ComparePasswordAndHash(password, string(hash))
 				if err != nil {
 					log.Println(err)
-					fmt.Fprint(w, "Error 500")
+					renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 					return
 				}
 				if !passMatch {
-					renderStatus(w, 403, acceptLanguage)
+					renderStatus(w, http.StatusForbidden, acceptLanguage)
 					return
 				}
 
-				// todo check duplicates
-
-				file, err := os.OpenFile(filepath.Join(stateDir, "s.toml"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
-				defer file.Close()
+				file, err := os.OpenFile(filepath.Join(stateDir, "s.toml"), os.O_RDONLY|os.O_CREATE, 0600)
 				if err != nil {
 					log.Println(err)
-					fmt.Fprint(w, "Error 500")
+					renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 					return
 				}
-				_, err = file.WriteString(id + " = " + url + "\n")
+				defer file.Close()
+
+				shorts := map[string]string{}
+				scanner := bufio.NewScanner(file)
+				for scanner.Scan() {
+					line := strings.Split(scanner.Text(), " = ")
+					shorts[line[0]] = line[1]
+				}
+				if err := scanner.Err(); err != nil {
+					log.Println(err)
+					renderStatus(w, http.StatusInternalServerError, acceptLanguage)
+					return
+				}
+				file.Close()
+
+				if _, ok := shorts[id]; ok {
+					renderStatus(w, http.StatusConflict, acceptLanguage)
+					return
+				}
+				shorts[id] = url
+
+				file, err = os.OpenFile(filepath.Join(stateDir, "s.toml"), os.O_WRONLY|os.O_TRUNC, 0600)
 				if err != nil {
 					log.Println(err)
-					fmt.Fprint(w, "Error 500")
+					renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 					return
 				}
+				defer file.Close()
+				for shortID, shortURL := range shorts {
+					_, err = file.WriteString(shortID + " = " + shortURL + "\n")
+					if err != nil {
+						log.Println(err)
+						renderStatus(w, http.StatusInternalServerError, acceptLanguage)
+						return
+					}
+				}
+
 				w.Header().Add("X-Redirection", "https://apiote.xyz/s/"+id)
-				w.WriteHeader(201)
+				w.WriteHeader(http.StatusCreated)
 				showHtml(w, "s201", Redirection{id, url, "https://apiote.xyz/s/" + id}, acceptLanguage)
 			}
 		} else if len(path) != 2 {
-			renderStatus(w, 404, acceptLanguage)
+			renderStatus(w, http.StatusNotFound, acceptLanguage)
 		} else {
 			id := strings.ToUpper(path[1])
-			data, err := ioutil.ReadFile(filepath.Join(stateDir, "s.toml"))
+			file, err := os.Open(filepath.Join(stateDir, "s.toml"))
 			if err != nil {
 				log.Println(err)
-				fmt.Fprint(w, "Error 500")
+				renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 				return
 			}
-			for _, line := range strings.Split(string(data), "\n") {
-				if line == "" {
-					break
+			defer file.Close()
+
+			scanner := bufio.NewScanner(file)
+			for scanner.Scan() {
+				line := strings.Split(scanner.Text(), " = ")
+				if len(line) < 2 {
+					continue
 				}
-				redirection := strings.Split(line, "=")
-				redirectionID := strings.Trim(redirection[0], " ")
-				redirectionURL := strings.Trim(redirection[1], " ")
-				if redirectionID == id {
-					http.Redirect(w, r, redirectionURL, http.StatusMovedPermanently)
+				if line[0] == id {
+					http.Redirect(w, r, line[1], http.StatusMovedPermanently)
 					return
 				}
 			}
-			renderStatus(w, 404, acceptLanguage)
+			renderStatus(w, http.StatusNotFound, acceptLanguage)
 		}
 	}
 }
@@ -554,27 +586,28 @@ 		acceptLanguage := r.Header.Get("Accept-Language")
 		path := strings.Split(r.URL.Path[1:], "/")
 		if len(path) == 2 && path[1] != "" {
 			id := strings.ToUpper(path[1])
-			data, err := ioutil.ReadFile(filepath.Join(stateDir, "s.toml"))
+			file, err := os.Open(filepath.Join(stateDir, "s.toml"))
 			if err != nil {
 				log.Println(err)
-				fmt.Fprint(w, "Error 500")
+				renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 				return
 			}
-			for _, line := range strings.Split(string(data), "\n") {
-				if line == "" {
-					break
+			defer file.Close()
+
+			scanner := bufio.NewScanner(file)
+			for scanner.Scan() {
+				line := strings.Split(scanner.Text(), " = ")
+				if len(line) < 2 {
+					continue
 				}
-				redirection := strings.Split(line, "=")
-				redirectionID := strings.Trim(redirection[0], " ")
-				redirectionURL := strings.Trim(redirection[1], " ")
-				if redirectionID == id {
-					showHtml(w, "short", Redirection{redirectionID, redirectionURL, "https://apiote.xyz/short/" + redirectionID}, acceptLanguage)
+				if line[0] == id {
+					showHtml(w, "short", Redirection{line[0], line[1], "https://apiote.xyz/short/" + line[0]}, acceptLanguage)
 					return
 				}
 			}
-			renderStatus(w, 404, acceptLanguage)
+			renderStatus(w, http.StatusNotFound, acceptLanguage)
 		} else {
-			renderStatus(w, 404, acceptLanguage)
+			renderStatus(w, http.StatusNotFound, acceptLanguage)
 		}
 	}
 }
@@ -583,16 +616,16 @@ func wkd(w http.ResponseWriter, r *http.Request) {
 	acceptLanguage := r.Header.Get("Accept-Language")
 	path := strings.Split(r.URL.Path[1:], "/")
 	if path[3] == "" {
-		renderStatus(w, 404, acceptLanguage)
+		renderStatus(w, http.StatusNotFound, acceptLanguage)
 	} else {
 		file, err := staticFS.Open("static/" + path[3] + ".pgp")
 		defer file.Close()
 		if err != nil && os.IsNotExist(err) {
-			renderStatus(w, 404, acceptLanguage)
+			renderStatus(w, http.StatusNotFound, acceptLanguage)
 			return
 		} else if err != nil {
 			log.Println(err)
-			fmt.Fprint(w, "Error 500")
+			renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 			return
 		}
 		w.Header().Set("Content-Type", "application/pgp-key")
@@ -600,7 +633,7 @@ 		w.Header().Set("Access-Control-Allow-Origin", "*")
 		_, err = io.Copy(w, file)
 		if err != nil {
 			log.Println(err)
-			fmt.Fprint(w, "Error 500")
+			renderStatus(w, http.StatusInternalServerError, acceptLanguage)
 		}
 	}
 }