| 1 |
package main |
| 2 |
|
| 3 |
import ( |
| 4 |
"github.com/jackc/pgx" |
| 5 |
log "gopkg.in/inconshreveable/log15.v2" |
| 6 |
"io/ioutil" |
| 7 |
"net/http" |
| 8 |
"os" |
| 9 |
) |
| 10 |
|
| 11 |
var pool *pgx.ConnPool |
| 12 |
|
| 13 |
// afterConnect creates the prepared statements that this application uses |
| 14 |
func afterConnect(conn *pgx.Conn) (err error) { |
| 15 |
_, err = conn.Prepare("getUrl", ` |
| 16 |
select url from shortened_urls where id=$1 |
| 17 |
`) |
| 18 |
if err != nil { |
| 19 |
return |
| 20 |
} |
| 21 |
|
| 22 |
_, err = conn.Prepare("deleteUrl", ` |
| 23 |
delete from shortened_urls where id=$1 |
| 24 |
`) |
| 25 |
if err != nil { |
| 26 |
return |
| 27 |
} |
| 28 |
|
| 29 |
// There technically is a small race condition in doing an upsert with a CTE |
| 30 |
// where one of two simultaneous requests to the shortened URL would fail |
| 31 |
// with a unique index violation. As the point of this demo is pgx usage and |
| 32 |
// not how to perfectly upsert in PostgreSQL it is deemed acceptable. |
| 33 |
_, err = conn.Prepare("putUrl", ` |
| 34 |
with upsert as ( |
| 35 |
update shortened_urls |
| 36 |
set url=$2 |
| 37 |
where id=$1 |
| 38 |
returning * |
| 39 |
) |
| 40 |
insert into shortened_urls(id, url) |
| 41 |
select $1, $2 where not exists(select 1 from upsert) |
| 42 |
`) |
| 43 |
return |
| 44 |
} |
| 45 |
|
| 46 |
func getUrlHandler(w http.ResponseWriter, req *http.Request) { |
| 47 |
var url string |
| 48 |
err := pool.QueryRow("getUrl", req.URL.Path).Scan(&url) |
| 49 |
switch err { |
| 50 |
case nil: |
| 51 |
http.Redirect(w, req, url, http.StatusSeeOther) |
| 52 |
case pgx.ErrNoRows: |
| 53 |
http.NotFound(w, req) |
| 54 |
default: |
| 55 |
http.Error(w, "Internal server error", http.StatusInternalServerError) |
| 56 |
} |
| 57 |
} |
| 58 |
|
| 59 |
func putUrlHandler(w http.ResponseWriter, req *http.Request) { |
| 60 |
id := req.URL.Path |
| 61 |
var url string |
| 62 |
if body, err := ioutil.ReadAll(req.Body); err == nil { |
| 63 |
url = string(body) |
| 64 |
} else { |
| 65 |
http.Error(w, "Internal server error", http.StatusInternalServerError) |
| 66 |
return |
| 67 |
} |
| 68 |
|
| 69 |
if _, err := pool.Exec("putUrl", id, url); err == nil { |
| 70 |
w.WriteHeader(http.StatusOK) |
| 71 |
} else { |
| 72 |
http.Error(w, "Internal server error", http.StatusInternalServerError) |
| 73 |
} |
| 74 |
} |
| 75 |
|
| 76 |
func deleteUrlHandler(w http.ResponseWriter, req *http.Request) { |
| 77 |
if _, err := pool.Exec("deleteUrl", req.URL.Path); err == nil { |
| 78 |
w.WriteHeader(http.StatusOK) |
| 79 |
} else { |
| 80 |
http.Error(w, "Internal server error", http.StatusInternalServerError) |
| 81 |
} |
| 82 |
} |
| 83 |
|
| 84 |
func urlHandler(w http.ResponseWriter, req *http.Request) { |
| 85 |
switch req.Method { |
| 86 |
case "GET": |
| 87 |
getUrlHandler(w, req) |
| 88 |
|
| 89 |
case "PUT": |
| 90 |
putUrlHandler(w, req) |
| 91 |
|
| 92 |
case "DELETE": |
| 93 |
deleteUrlHandler(w, req) |
| 94 |
|
| 95 |
default: |
| 96 |
w.Header().Add("Allow", "GET, PUT, DELETE") |
| 97 |
w.WriteHeader(http.StatusMethodNotAllowed) |
| 98 |
} |
| 99 |
} |
| 100 |
|
| 101 |
func main() { |
| 102 |
var err error |
| 103 |
connPoolConfig := pgx.ConnPoolConfig{ |
| 104 |
ConnConfig: pgx.ConnConfig{ |
| 105 |
Host: "127.0.0.1", |
| 106 |
User: "jack", |
| 107 |
Password: "jack", |
| 108 |
Database: "url_shortener", |
| 109 |
Logger: log.New("module", "pgx"), |
| 110 |
}, |
| 111 |
MaxConnections: 5, |
| 112 |
AfterConnect: afterConnect, |
| 113 |
} |
| 114 |
pool, err = pgx.NewConnPool(connPoolConfig) |
| 115 |
if err != nil { |
| 116 |
log.Crit("Unable to create connection pool", "error", err) |
| 117 |
os.Exit(1) |
| 118 |
} |
| 119 |
|
| 120 |
http.HandleFunc("/", urlHandler) |
| 121 |
|
| 122 |
log.Info("Starting URL shortener on localhost:8080") |
| 123 |
err = http.ListenAndServe("localhost:8080", nil) |
| 124 |
if err != nil { |
| 125 |
log.Crit("Unable to start web server", "error", err) |
| 126 |
os.Exit(1) |
| 127 |
} |
| 128 |
} |