aboutsummaryrefslogtreecommitdiff
path: root/src/short.nim
blob: 5005e16f03811e884a2c6fc1598b04320c6f230e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import os, strutils
import std/[hashes, re, times, uri]

import tiny_sqlite
import jester
import nimja/parser
import uuids

import database

const allCss = staticRead("../static/all.css")
const cssRoute = "/static/all.css." & $hash(allCss)
const favicon = staticRead("../static/favicon.ico")
const faviconSvg = staticRead("../static/favicon.svg")

var db {.threadvar.}: DbConn

proc initDB() {.raises: [SqliteError].} =
  if not db.isOpen():
    db = openDatabase("data/short.db")
    if not db.Migrate():
      echo "Failed to migrate database schema"
      quit 1

func renderIndex(): string {.raises: [].} =
  var req: ShortUrl
  compileTemplateFile(getScriptDir() / "templates/index.html")

func renderAbout(): string {.raises: [].} =
  compileTemplateFile(getScriptDir() / "templates/about.html")

func renderShort(req: ShortUrl): string {.raises: [].} =
  compileTemplateFile(getScriptDir() / "templates/short.html")

func renderNoShort(req: ShortUrl): string {.raises: [].} =
  compileTemplateFile(getScriptDir() / "templates/noshort.html")

func renderError(code: int, msg: string): string {.raises: [].} =
  compileTemplateFile(getScriptDir() / "templates/error.html")

proc handleToken(tokenStr: string): (HttpCode, string) {.raises: [].} =
  var token: UUID
  try:
    token = parseUUID(tokenStr)
  except ValueError:
    return (Http400, renderError(400, "Bad Request"))
  db.CleanExpired()
  try:
    let req = db.GetUrl(token)
    if req == nil:
      return (Http404, renderNoShort(req[]))
    return (Http200, renderShort(req[]))
  except SqliteError:
    return (Http500, renderError(500, "SqliteError"))

proc handleIndexPost(params: Table[string, string]): (HttpCode, string) {.raises: [].} =
  var input: ShortUrl
  var exp: int
  for k, v in params.pairs:
    case k:
      of "title":
        try:
          let titleRegexp = re"^[\w\s]{3,64}$"
          if match(v, titleRegexp):
            input.Title = v
          else:
            return (Http400, renderError(400, "Bad Request"))
        except RegexError:
          return (Http500, renderError(500, "RegexError"))
      of "url":
        try:
          discard parseUri(v)
          input.Url = v
        except:
          return (Http400, renderError(400, "Bad Request"))
      of "expires":
        try:
          exp = parseInt(v)
        except ValueError:
          return (Http400, renderError(400, "Bad Request"))
        if exp < 1 or exp > 527040:
          return (Http400, renderError(400, "Bad Request"))
      of "shorten": discard
      else: return (Http400, renderError(400, "Bad Request"))
  if input.Title == "" or input.Url == "" or exp == 0:
    return (Http400, renderError(400, "Bad Request"))
  try:
    input.Token = genUUID()
  except IOError:
    return (Http500, renderError(500, "IOError on genUUID"))
  except OSError:
    return (Http500, renderError(500, "OSError on genUUID"))
  input.Created = times.now()
  input.Expires = input.Created + initDuration(minutes = exp)
  try:
    db.AddUrl(input)
  except SqliteError:
    return (Http500, renderShort(input))
  return (Http200, $input.Token)

routes:
  get "/":
    resp renderIndex()
  get "/about":
    resp renderAbout()
  post "/":
    initDB()
    var (code, content) = handleIndexPost(request.params)
    if code != Http200:
      resp code, content
    else:
      redirect("/" & content)
  get "/static/favicon.ico":
    resp Http200, {"content-type": "image/x-icon"}, favicon
  get "/static/favicon.svg":
    resp Http200, {"content-type": "image/svg+xml"}, faviconSvg
  get re"^/static/all\.css\.":
    resp Http200, {"content-type": "text/css"}, allcss
  get "/@token":
    initDB()
    var (code, content) = handleToken(@"token")
    resp code, content

when isMainModule:
  runForever()