2022-03-20 00:47:01 +00:00
|
|
|
package links
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-03-20 01:49:41 +00:00
|
|
|
"log"
|
2022-03-20 00:47:01 +00:00
|
|
|
"net/url"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/Masterminds/squirrel"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/jackc/pgx/v4"
|
|
|
|
"github.com/jackc/pgx/v4/pgxpool"
|
|
|
|
)
|
|
|
|
|
|
|
|
var psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
|
|
|
|
|
|
|
type URL struct {
|
|
|
|
ID string
|
|
|
|
URL string
|
|
|
|
NURL string // normalized URL
|
|
|
|
Short string
|
|
|
|
CreatedAt time.Time
|
|
|
|
}
|
|
|
|
|
2022-03-20 22:03:39 +00:00
|
|
|
func (u *URL) Timestamp() string {
|
|
|
|
return u.CreatedAt.Format("2006-01-02 15:04")
|
|
|
|
}
|
|
|
|
|
2022-03-20 01:49:41 +00:00
|
|
|
func (u *URL) Store(ctx context.Context, db *pgxpool.Pool) error {
|
2022-03-20 00:47:01 +00:00
|
|
|
stmt := psql.Insert("urls").
|
|
|
|
Columns("id", "url", "nurl", "short", "created_at").
|
2022-03-20 01:49:41 +00:00
|
|
|
Values(u.ID, u.URL, u.NURL, u.Short, u.CreatedAt)
|
2022-03-20 00:47:01 +00:00
|
|
|
query, args, err := stmt.ToSql()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-03-20 01:49:41 +00:00
|
|
|
_, err = db.Exec(ctx, query, args...)
|
2022-03-20 00:47:01 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-09-13 05:37:04 +00:00
|
|
|
// Normalize cleans the URL to only the parts we care about.
|
2022-03-20 00:47:01 +00:00
|
|
|
func Normalize(u *url.URL) *url.URL {
|
|
|
|
norm := &url.URL{
|
|
|
|
Scheme: u.Scheme,
|
|
|
|
Host: u.Host,
|
|
|
|
Path: u.Path,
|
|
|
|
RawPath: u.RawPath,
|
|
|
|
}
|
|
|
|
return norm
|
|
|
|
}
|
|
|
|
|
|
|
|
func NormalizeString(s string) (string, error) {
|
|
|
|
u, err := url.Parse(s)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
u = Normalize(u)
|
|
|
|
return u.String(), nil
|
|
|
|
}
|
|
|
|
|
2022-03-20 22:03:39 +00:00
|
|
|
// Clean should scrub out junk from the URL.
|
2023-09-13 05:37:04 +00:00
|
|
|
func Clean(u *url.URL, keepQuery bool) *url.URL {
|
2022-03-20 22:03:39 +00:00
|
|
|
norm := &url.URL{
|
|
|
|
Scheme: u.Scheme,
|
|
|
|
Host: u.Host,
|
|
|
|
Path: u.Path,
|
|
|
|
RawPath: u.RawPath,
|
|
|
|
Fragment: u.Fragment,
|
|
|
|
RawFragment: u.RawFragment,
|
|
|
|
}
|
2023-09-13 05:37:04 +00:00
|
|
|
|
|
|
|
if keepQuery {
|
|
|
|
norm.RawQuery = u.RawQuery
|
|
|
|
}
|
2022-03-20 22:03:39 +00:00
|
|
|
return norm
|
|
|
|
}
|
|
|
|
|
2023-09-13 05:37:04 +00:00
|
|
|
func CleanString(s string, isRawURL bool) (string, error) {
|
2022-03-20 22:03:39 +00:00
|
|
|
u, err := url.Parse(s)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2023-09-13 05:37:04 +00:00
|
|
|
u = Clean(u, isRawURL)
|
2022-03-20 22:03:39 +00:00
|
|
|
return u.String(), nil
|
|
|
|
}
|
|
|
|
|
2022-03-20 00:47:01 +00:00
|
|
|
func New(u *url.URL) *URL {
|
2022-03-20 01:49:41 +00:00
|
|
|
link := &URL{
|
2022-03-20 00:47:01 +00:00
|
|
|
ID: uuid.NewString(),
|
|
|
|
URL: u.String(),
|
|
|
|
NURL: Normalize(u).String(),
|
|
|
|
Short: GenCode(),
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
}
|
|
|
|
|
|
|
|
return link
|
|
|
|
}
|
|
|
|
|
|
|
|
func FromString(s string) (*URL, error) {
|
|
|
|
u, err := url.Parse(s)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return New(u), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func Lookup(ctx context.Context, db *pgxpool.Pool, s string) (string, error) {
|
|
|
|
u, err := url.Parse(s)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
u = Normalize(u)
|
|
|
|
|
|
|
|
stmt := psql.Select("short").From("urls").
|
|
|
|
Where(squirrel.Eq{"nurl": u.String()})
|
|
|
|
query, args, err := stmt.ToSql()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
row := db.QueryRow(ctx, query, args...)
|
|
|
|
|
|
|
|
var short string
|
2022-03-20 01:49:41 +00:00
|
|
|
err = row.Scan(&short)
|
2022-03-20 00:47:01 +00:00
|
|
|
if err != nil {
|
2022-03-20 01:49:41 +00:00
|
|
|
return "", err
|
2022-03-20 00:47:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return short, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func StoreURL(ctx context.Context, db *pgxpool.Pool, s string) (string, error) {
|
|
|
|
short, err := Lookup(ctx, db, s)
|
|
|
|
if err == nil {
|
|
|
|
return short, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != pgx.ErrNoRows {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
u, err := FromString(s)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = u.Store(ctx, db)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return u.Short, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func RetrieveURL(ctx context.Context, db *pgxpool.Pool, short string) (string, error) {
|
2022-03-20 01:49:41 +00:00
|
|
|
log.Printf("look up url for short code %s", short)
|
|
|
|
stmt := psql.Select("url").From("urls").
|
|
|
|
Where(squirrel.Eq{"short": short})
|
|
|
|
query, args, err := stmt.ToSql()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
row := db.QueryRow(ctx, query, args...)
|
|
|
|
var url string
|
|
|
|
|
|
|
|
err = row.Scan(&url)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2022-03-20 00:47:01 +00:00
|
|
|
|
2022-03-20 01:49:41 +00:00
|
|
|
return url, nil
|
2022-03-20 00:47:01 +00:00
|
|
|
}
|
2022-03-20 22:03:39 +00:00
|
|
|
|
|
|
|
func FetchAll(ctx context.Context, db *pgxpool.Pool) ([]*URL, error) {
|
|
|
|
stmt := psql.Select("*").From("urls").OrderBy("created_at")
|
|
|
|
query, args, err := stmt.ToSql()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
rows, err := db.Query(ctx, query, args...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var urls []*URL
|
|
|
|
for rows.Next() {
|
|
|
|
u := &URL{}
|
|
|
|
err = rows.Scan(
|
|
|
|
&u.ID,
|
|
|
|
&u.URL,
|
|
|
|
&u.NURL,
|
|
|
|
&u.Short,
|
|
|
|
&u.CreatedAt,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
urls = append(urls, u)
|
|
|
|
}
|
|
|
|
|
|
|
|
return urls, nil
|
|
|
|
}
|