helperfuncs.go

  1// SPDX-FileCopyrightText: 2022 Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: BSD-2-Clause
  4
  5package main
  6
  7import (
  8	"errors"
  9	"fmt"
 10	"log"
 11	"net/url"
 12	"sort"
 13	"strings"
 14	"text/template"
 15
 16	"github.com/dgraph-io/badger/v3"
 17)
 18
 19// Create shortens a given URL with an optional name. If a name is provided,
 20// that name will be used. Otherwise, 4-character string will be generated and
 21// used instead.
 22func (m model) create(name string, url string) string {
 23	err := m.database.Update(func(txn *badger.Txn) error {
 24		return txn.Set([]byte(name), []byte(url))
 25	})
 26	if err != nil {
 27		// TODO: return an error instead so it can be handled like an error
 28		return fmt.Sprint(err)
 29	}
 30
 31	log.Println("\"" + url + "\" mapped to \"" + name + "\"")
 32	return fmt.Sprint("URL mapped to ", name, "\n")
 33}
 34
 35// read() accepts a start variable for the first link in a set (implying reverse
 36// iteration), an end variable for the last link in a set (implying forward
 37// iteration), and a count variable for the number of requested entries. The
 38// requested set is returned as JSON.
 39func (m model) read(start string, end string, count int) (map[string]string, error) {
 40	links := make(map[string]string)
 41
 42	if !(count > 0) {
 43		return nil, errors.New("Count parameter is required")
 44	}
 45
 46	err := m.database.View(func(txn *badger.Txn) error {
 47		opts := badger.DefaultIteratorOptions
 48		opts.PrefetchSize = 20
 49		if start != "" && end == "" {
 50			// Start value provided, iterate backwards
 51			opts.Reverse = true
 52		}
 53		iterator := txn.NewIterator(opts)
 54		defer iterator.Close()
 55		if start == "" && end == "" {
 56			iterator.Rewind()
 57		} else if start != "" && end == "" {
 58			// Set position to "start"
 59			iterator.Seek([]byte(start))
 60			// If "start" exists, move to next value
 61			if iterator.Valid() {
 62				iterator.Next()
 63			}
 64		} else if start == "" && end != "" {
 65			// Start value provided, iterate forwards
 66			// Set position to "end"
 67			iterator.Seek([]byte(end))
 68			// If "end" exists, move to next value
 69			if iterator.Valid() {
 70				iterator.Next()
 71			}
 72		} else {
 73			return errors.New("Only provide start OR end parameters, not both")
 74		}
 75		for i := 0; i < count; i++ {
 76			if iterator.Valid() {
 77				err := iterator.Item().Value(func(v []byte) error {
 78					links[string(iterator.Item().Key())] = string(v)
 79					return nil
 80				})
 81				if err != nil {
 82					return err
 83				}
 84				iterator.Next()
 85			}
 86		}
 87		return nil
 88	})
 89	if err != nil {
 90		return nil, err
 91	}
 92
 93	return links, nil
 94}
 95
 96// Update modifies a shortened link
 97func (m model) update(name string, url string, oldName string) string {
 98	m.create(name, url)
 99	if name != oldName {
100		m.delete(oldName)
101	}
102	return fmt.Sprint("\"" + url + "\" mapped to \"" + name + "\"")
103}
104
105// Delete removes a shortened URL from the database given its name.
106func (m model) delete(name string) string {
107	err := m.database.Update(func(txn *badger.Txn) error {
108		return txn.Delete([]byte(name))
109	})
110	if err != nil {
111		return fmt.Sprint(err)
112	}
113
114	log.Println("\"" + name + "\" has been deleted")
115	return fmt.Sprint("\"" + name + "\" has been deleted\n")
116}
117
118// nameExists returns true if the provided name is already stored in the
119// database and false if it's not.
120func (m model) nameExists(name string) bool {
121	err := m.database.View(func(txn *badger.Txn) error {
122		_, err := txn.Get([]byte(name))
123		if err != nil {
124			return err
125		}
126		return nil
127	})
128	return err == nil
129}
130
131// unauthenticated serves the unauthenticated home page to the visitor
132func unauthenticated() string {
133	home, err := templates.ReadFile("templates/home_unauthenticated.html")
134	if err != nil {
135		log.Fatalln(err)
136	}
137	return string(home)
138}
139
140// authenticated serves the authenticated dashboard to the user
141func (m model) authenticated(links map[string]string) string {
142	dash, err := templates.ReadFile("templates/home_authenticated.html")
143	if err != nil {
144		log.Fatalln(err)
145	}
146	tmpl, err := template.New("authenticated").Parse(string(dash))
147	if err != nil {
148		log.Println(err)
149	}
150
151	// Maps have no order so we're making an array containing the keys
152	linksArray := make([]string, 0, len(links))
153	for k := range links {
154		linksArray = append(linksArray, k)
155	}
156	// Then sorting them
157	sort.Sort(sort.StringSlice(linksArray))
158
159	// And ranging through the keys to pull ordered values from the map
160	var table string
161	for _, k := range linksArray {
162		v := links[k]
163		table = table + fmt.Sprintf(`<tr>
164	<td><p>%s</p></td>
165	<td><p>%s</p></td>
166	<td><a class="button" href="/?action=edit&name=%s&url=%s">Edit</a><a class="button" href="/?action=delete&name=%s">Delete</a></td>
167</tr>`, k, v, k, url.QueryEscape(string(v)), k)
168	}
169
170	page := new(strings.Builder)
171	err = tmpl.Execute(page, table)
172	if err != nil {
173		log.Println(err)
174	}
175	return page.String()
176}
177
178type EditFields struct {
179	URL  string
180	Name string
181}
182
183// update serves the editing interface to the user
184func (m model) edit(name string, url string) string {
185	fields := EditFields{url, name}
186	editPage, err := templates.ReadFile("templates/edit.html")
187	if err != nil {
188		log.Fatalln(err)
189	}
190	tmpl, err := template.New("editPage").Parse(string(editPage))
191	if err != nil {
192		log.Println(err)
193	}
194	page := new(strings.Builder)
195	err = tmpl.Execute(page, fields)
196	if err != nil {
197		log.Println(err)
198	}
199	return page.String()
200}