blob: 58c022b128c7d1fd6ec64ab0b0fa2074e0f241aa [file] [log] [blame]
DTabidzeb00a1db2024-01-12 18:30:14 +04001package main
2
3import (
4 "database/sql"
5 "embed"
6 "flag"
7 "fmt"
8 "html/template"
9 "log"
10 "math/rand"
11 "net/http"
12 "strings"
13
14 "github.com/mattn/go-sqlite3"
15)
16
17var dbPath = flag.String("db-path", "url-shortener.db", "Path to the SQLite file")
18
19//go:embed index.html
20var indexHTML embed.FS
21
22type NamedAddress struct {
23 Name string
24 Address string
25 OwnerId string
26 Active bool
27}
28
29type Store interface {
30 Create(addr NamedAddress) error
31 Get(name string) (NamedAddress, error)
32 Activate(name string) error
33 Deactivate(name string) error
34 ChangeOwner(name, ownerId string) error
35 List(ownerId string) ([]NamedAddress, error)
36}
37
38type NameAlreadyTaken struct {
39 Name string
40}
41
42func (er NameAlreadyTaken) Error() string {
43 return fmt.Sprintf("Name '%s' is already taken", er.Name)
44}
45
46type SQLiteStore struct {
47 db *sql.DB
48}
49
50func NewSQLiteStore(path string) (*SQLiteStore, error) {
51 db, err := sql.Open("sqlite3", path)
52 if err != nil {
53 return nil, err
54 }
55
56 _, err = db.Exec(`
57 CREATE TABLE IF NOT EXISTS named_addresses (
58 name TEXT PRIMARY KEY,
59 address TEXT,
60 owner_id TEXT,
61 active BOOLEAN
62 )
63 `)
64 if err != nil {
65 return nil, err
66 }
67
68 return &SQLiteStore{db: db}, nil
69}
70
71func generateRandomURL() string {
72 const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
73 var urlShort string
74 for i := 0; i < 6; i++ {
75 urlShort += string(charset[rand.Intn(len(charset))])
76 }
77 return urlShort
78}
79
80func (s *SQLiteStore) Create(addr NamedAddress) error {
81 _, err := s.db.Exec(`
82 INSERT INTO named_addresses (name, address, owner_id, active)
83 VALUES (?, ?, ?, ?)
84 `, addr.Name, addr.Address, addr.OwnerId, addr.Active)
85 if err != nil {
86 sqliteErr, ok := err.(sqlite3.Error)
87 // sqliteErr.ExtendedCode and sqlite3.ErrConstraintUnique are not the same. probably some lib error.
88 // had to use actual code of unique const error
89 if ok && sqliteErr.ExtendedCode == 1555 {
90 return NameAlreadyTaken{Name: addr.Name}
91 }
92 return err
93 }
94 return nil
95}
96
97func (s *SQLiteStore) Get(name string) (NamedAddress, error) {
98 row := s.db.QueryRow("SELECT name, address, owner_id, active FROM named_addresses WHERE name = ?", name)
99 namedAddress := NamedAddress{}
100 err := row.Scan(&namedAddress.Name, &namedAddress.Address, &namedAddress.OwnerId, &namedAddress.Active)
101 if err != nil {
102 if err == sql.ErrNoRows {
103 return NamedAddress{}, fmt.Errorf("No record found for name %s", name)
104 }
105 return NamedAddress{}, err
106 }
107 return namedAddress, nil
108}
109
110func (s *SQLiteStore) Activate(name string) error {
111 //TODO
112 return nil
113}
114
115func (s *SQLiteStore) Deactivate(name string) error {
116 //TODO
117 return nil
118}
119
120func (s *SQLiteStore) ChangeOwner(name, ownerId string) error {
121 //TODO
122 return nil
123}
124
125func (s *SQLiteStore) List(ownerId string) ([]NamedAddress, error) {
126 rows, err := s.db.Query("SELECT name, address, owner_id, active FROM named_addresses WHERE owner_id = ?", ownerId)
127 if err != nil {
128 return nil, err
129 }
130 defer rows.Close()
131 var namedAddresses []NamedAddress
132 for rows.Next() {
133 var namedAddress NamedAddress
134 if err := rows.Scan(&namedAddress.Name, &namedAddress.Address, &namedAddress.OwnerId, &namedAddress.Active); err != nil {
135 return nil, err
136 }
137 namedAddresses = append(namedAddresses, namedAddress)
138 }
139 return namedAddresses, nil
140}
141
142type PageVariables struct {
143 NamedAddresses []NamedAddress
144}
145
146func renderHTML(w http.ResponseWriter, r *http.Request, tpl *template.Template, data interface{}) {
147 w.Header().Set("Content-Type", "text/html")
148 err := tpl.Execute(w, data)
149 if err != nil {
150 http.Error(w, err.Error(), http.StatusInternalServerError)
151 }
152}
153
154type Server struct {
155 store Store
156}
157
158func (s *Server) Start() {
159 http.HandleFunc("/", s.handler)
160 log.Fatal(http.ListenAndServe(":8080", nil))
161}
162
163func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
164 if r.Method == http.MethodPost {
165 customName := r.PostFormValue("custom")
166 address := r.PostFormValue("address")
167 if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
168 http.Error(w, "Address must start with http:// or https://", http.StatusBadRequest)
169 return
170 }
171 for {
172 cn := customName
173 if cn == "" {
174 cn = generateRandomURL()
175 }
176 // check if custom exists
177 namedAddress := NamedAddress{
178 Name: cn,
179 Address: address,
180 OwnerId: "tabo", //TODO. Owner ID should be taken from http header
181 Active: true,
182 }
183 if err := s.store.Create(namedAddress); err == nil {
184 http.Redirect(w, r, "/", http.StatusSeeOther)
185 return
186 } else if _, ok := err.(NameAlreadyTaken); ok && customName == "" {
187 continue
188 } else if _, ok := err.(NameAlreadyTaken); ok && customName != "" {
189 http.Error(w, "Name is already taken", http.StatusBadRequest)
190 return
191 } else {
192 http.Error(w, "Try again later", http.StatusInternalServerError)
193 return
194 }
195 }
196 }
197 // Get Name from request path for redirection
198 name := strings.TrimPrefix(r.URL.Path, "/")
199 if name != "" {
200 namedAddress, err := s.store.Get(name)
201 if err != nil {
202 return
203 }
204 // Redirect to the address
205 http.Redirect(w, r, namedAddress.Address, http.StatusSeeOther)
206 return
207 }
208 // Retrieve named addresses for the owner
209 namedAddresses, err := s.store.List("tabo")
210 if err != nil {
211 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
212 return
213 }
214 // Combine data for rendering
215 pageVariables := PageVariables{
216 NamedAddresses: namedAddresses,
217 }
218 indexHtmlContent, err := indexHTML.ReadFile("index.html")
219 if err != nil {
220 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
221 return
222 }
223 tmpl, err := template.New("index").Parse(string(indexHtmlContent))
224 if err != nil {
225 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
226 return
227 }
228 renderHTML(w, r, tmpl, pageVariables)
229}
230
231func main() {
232 flag.Parse()
233 db, err := NewSQLiteStore(*dbPath)
234 if err != nil {
235 panic(err)
236 }
237 s := Server{store: db}
238 s.Start()
239}