Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit ae3e0bf

Browse files
authored
Merge pull request #4 from itsjoniur/dev
First release of Bitlygo
2 parents 1d8e453 + 09ec1bc commit ae3e0bf

File tree

26 files changed

+1057
-444
lines changed

26 files changed

+1057
-444
lines changed

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ _testmain.go
2424
*.prof
2525

2626
tern.conf
27+
internal/configs/config.yaml
28+
*.log

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ Otherwise, we should alert that is a 404 (HTTP Status) route and display a 404 w
129129

130130
### `UPDATE /:name`
131131

132-
- STRING `new_name` (required)
132+
- STRING `new_name` (optional)
133133

134134
- STRING `link` (required, and we will check the link should be valid and pass URL standard format)
135135
About link value: we must support **UTF-8** characters or query values.

‎api/link.go‎

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/url"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/go-chi/chi/v5"
11+
"github.com/itsjoniur/bitlygo/internal/models"
12+
"github.com/itsjoniur/bitlygo/internal/responses"
13+
"github.com/itsjoniur/bitlygo/pkg/strutil"
14+
)
15+
16+
func addLinkHandler(w http.ResponseWriter, req *http.Request) {
17+
type Params struct {
18+
Name string `json:"name"`
19+
Link string `json:"link"`
20+
}
21+
var err error
22+
link := &models.Link{}
23+
apiKey := req.Header.Get("API-KEY")
24+
25+
params := Params{}
26+
json.NewDecoder(req.Body).Decode(&params)
27+
28+
params.Name, err = strutil.RemoveNonAlphanumerical(params.Name)
29+
if err != nil {
30+
responses.BadRequestError(req.Context(), w)
31+
return
32+
}
33+
34+
if params.Name == "" {
35+
// Generate random string
36+
params.Name = strutil.RandStringRunes(8)
37+
}
38+
39+
if params.Link == "" {
40+
// Link is a required field and when it's empty we should return an error
41+
responses.BadRequestError(req.Context(), w)
42+
return
43+
}
44+
45+
if _, err := url.ParseRequestURI(params.Link); err != nil {
46+
responses.InvalidLinkError(req.Context(), w)
47+
return
48+
}
49+
50+
if apiKey != "" {
51+
link, err = models.CreateLink(req.Context(), 0, params.Name, params.Link)
52+
} else {
53+
link, err = models.CreateLinkWithExpireTime(req.Context(), 0, params.Name, params.Link)
54+
}
55+
if err != nil && strings.Contains(string(err.Error()), "duplicate key") {
56+
responses.LinkIsExistsError(req.Context(), w, params.Name)
57+
return
58+
}
59+
60+
if err != nil {
61+
responses.InternalServerError(req.Context(), w)
62+
return
63+
}
64+
65+
responses.RenderNewLinkResponse(req.Context(), w, link)
66+
}
67+
68+
func addLinkByPathHandler(w http.ResponseWriter, req *http.Request) {
69+
type Params struct {
70+
Name string `json:"name"`
71+
Link string `json:"link"`
72+
}
73+
var err error
74+
link := &models.Link{}
75+
params := Params{}
76+
apiKey := req.Header.Get("API-KEY")
77+
78+
json.NewDecoder(req.Body).Decode(&params)
79+
if params.Link == "" {
80+
responses.FieldEmptyError(req.Context(), w, "link")
81+
return
82+
}
83+
84+
params.Name = chi.URLParam(req, "name")
85+
params.Name, err = strutil.RemoveNonAlphanumerical(params.Name)
86+
if err != nil {
87+
responses.BadRequestError(req.Context(), w)
88+
return
89+
}
90+
91+
if params.Name == "" {
92+
params.Name = strutil.RandStringRunes(8)
93+
}
94+
95+
if _, err := url.ParseRequestURI(params.Link); err != nil {
96+
responses.InvalidLinkError(req.Context(), w)
97+
return
98+
}
99+
100+
if apiKey != "" {
101+
link, err = models.CreateLink(req.Context(), 0, params.Name, params.Link)
102+
} else {
103+
link, err = models.CreateLinkWithExpireTime(req.Context(), 0, params.Name, params.Link)
104+
}
105+
if err != nil && strings.Contains(string(err.Error()), "duplicate key") {
106+
responses.LinkIsExistsError(req.Context(), w, params.Name)
107+
return
108+
}
109+
110+
if err != nil {
111+
responses.InternalServerError(req.Context(), w)
112+
return
113+
}
114+
115+
responses.RenderNewLinkResponse(req.Context(), w, link)
116+
}
117+
118+
func updateLinkHandler(w http.ResponseWriter, req *http.Request) {
119+
type Params struct {
120+
NewName string `json:"new_name"`
121+
Link string `json:"link"`
122+
}
123+
var err error
124+
params := Params{}
125+
name := chi.URLParam(req, "name")
126+
127+
name, err = strutil.RemoveNonAlphanumerical(name)
128+
if err != nil {
129+
responses.BadRequestError(req.Context(), w)
130+
return
131+
}
132+
133+
isExist := models.GetLinkByName(req.Context(), name)
134+
if isExist == nil {
135+
responses.NotFoundError(req.Context(), w)
136+
return
137+
}
138+
139+
json.NewDecoder(req.Body).Decode(&params)
140+
if params.Link == "" {
141+
responses.FieldEmptyError(req.Context(), w, "link")
142+
return
143+
}
144+
145+
link, err := models.UpdateLinkByName(req.Context(), name, params.NewName, params.Link)
146+
if err != nil && strings.Contains(string(err.Error()), "duplicate key") {
147+
responses.LinkIsExistsError(req.Context(), w, params.NewName)
148+
return
149+
}
150+
if err != nil {
151+
responses.InternalServerError(req.Context(), w)
152+
return
153+
}
154+
155+
responses.RenderNewLinkResponse(req.Context(), w, link)
156+
}
157+
158+
func deleteLinkHandler(w http.ResponseWriter, req *http.Request) {
159+
var err error
160+
name := chi.URLParam(req, "name")
161+
162+
name, err = strutil.RemoveNonAlphanumerical(name)
163+
if err != nil {
164+
responses.BadRequestError(req.Context(), w)
165+
return
166+
}
167+
168+
if name == "" {
169+
responses.BadRequestError(req.Context(), w)
170+
return
171+
}
172+
173+
err = models.DeleteLinkByName(req.Context(), name)
174+
if err != nil {
175+
responses.InternalServerError(req.Context(), w)
176+
return
177+
}
178+
179+
json.NewEncoder(w).Encode(map[string]bool{"status": true})
180+
}
181+
182+
func searchLinkHandler(w http.ResponseWriter, req *http.Request) {
183+
var err error
184+
sq := req.URL.Query().Get("q")
185+
limit := req.URL.Query().Get("limit")
186+
187+
sq, err = strutil.RemoveNonAlphanumerical(sq)
188+
if err != nil {
189+
responses.BadRequestError(req.Context(), w)
190+
return
191+
}
192+
193+
if len(sq) < 1 {
194+
responses.FieldEmptyError(req.Context(), w, "search")
195+
return
196+
}
197+
198+
if limit == "" {
199+
limit = "10"
200+
}
201+
202+
l, err := strconv.Atoi(limit)
203+
if err != nil {
204+
responses.InternalServerError(req.Context(), w)
205+
return
206+
}
207+
208+
if 1 > l || l > 100 {
209+
responses.LimitRangeError(req.Context(), w)
210+
return
211+
}
212+
213+
links, err := models.SearchLinkByName(req.Context(), sq, l)
214+
if err != nil {
215+
responses.InternalServerError(req.Context(), w)
216+
return
217+
}
218+
219+
responses.RenderSearchLinkResponse(req.Context(), w, links)
220+
221+
}
222+
223+
func showTopLinksHandler(w http.ResponseWriter, req *http.Request) {
224+
limit := req.URL.Query().Get("limit")
225+
226+
if limit == "" {
227+
limit = "10"
228+
}
229+
230+
l, err := strconv.Atoi(limit)
231+
if err != nil {
232+
responses.InternalServerError(req.Context(), w)
233+
return
234+
}
235+
236+
if 1 > l || 1 > 100 {
237+
responses.LimitRangeError(req.Context(), w)
238+
return
239+
}
240+
241+
tl, err := models.TopLinksByVisits(req.Context(), l)
242+
if err != nil {
243+
responses.InternalServerError(req.Context(), w)
244+
return
245+
}
246+
247+
responses.RenderTopLinksResponse(req.Context(), w, tl)
248+
}
249+
250+
func redirectHandler(w http.ResponseWriter, req *http.Request) {
251+
var err error
252+
name := chi.URLParam(req, "name")
253+
254+
name, err = strutil.RemoveNonAlphanumerical(name)
255+
if err != nil {
256+
responses.BadRequestError(req.Context(), w)
257+
return
258+
}
259+
260+
if name == "" {
261+
responses.BadRequestError(req.Context(), w)
262+
return
263+
}
264+
265+
link := models.GetLinkByName(req.Context(), name)
266+
if link == nil {
267+
responses.NotFoundError(req.Context(), w)
268+
return
269+
}
270+
271+
go models.AddViewToLinkByName(req.Context(), name)
272+
273+
http.Redirect(w, req, link.Link, http.StatusPermanentRedirect)
274+
}
275+
276+
func showExpireSoonLinksHandler(w http.ResponseWriter, req *http.Request) {
277+
links, err := models.GetExpireSoonLinks(req.Context())
278+
if err != nil {
279+
responses.InternalServerError(req.Context(), w)
280+
return
281+
}
282+
283+
responses.RenderExpireLinkResponse(req.Context(), w, links)
284+
}

‎api/root.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package api
2+
3+
import "net/http"
4+
5+
func rootHandler(w http.ResponseWriter, req *http.Request) {
6+
w.Write([]byte("documentation"))
7+
}

‎api/server.go‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
8+
"github.com/go-chi/chi/v5"
9+
"github.com/go-chi/chi/v5/middleware"
10+
"github.com/itsjoniur/bitlygo/internal/durable"
11+
"github.com/itsjoniur/bitlygo/internal/middlewares"
12+
"github.com/jackc/pgx/v4/pgxpool"
13+
"github.com/unrolled/render"
14+
)
15+
16+
func StartAPI(logger *durable.Logger, db *pgxpool.Pool, port string) error {
17+
router := chi.NewRouter()
18+
database := durable.WrapDatabase(db)
19+
// setup middlewares
20+
router.Use(middlewares.Logger(logger)) //fs logger
21+
router.Use(middlewares.Header)
22+
router.Use(middlewares.ContextMiddleware(database))
23+
router.Use(middlewares.Render(render.New()))
24+
router.Use(middleware.Logger) // http requests logger
25+
router.Use(middleware.StripSlashes)
26+
router.Use(middleware.Recoverer)
27+
// register routes
28+
router.Get("/", rootHandler)
29+
router.Post("/add", addLinkHandler)
30+
router.Post("/{name}", addLinkByPathHandler)
31+
router.Put("/{name}", updateLinkHandler)
32+
router.Delete("/{name}", deleteLinkHandler)
33+
router.Get("/{name}", redirectHandler)
34+
router.Get("/search", searchLinkHandler)
35+
router.Get("/top", showTopLinksHandler)
36+
router.Get("/expire-soon", showExpireSoonLinksHandler)
37+
38+
log.Printf("Server running on %v port...", port)
39+
if err := http.ListenAndServe(fmt.Sprintf(":%s", port), router); err != nil {
40+
return err
41+
}
42+
43+
return nil
44+
}

‎cmd/bitlygo/main.go‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"path"
7+
"path/filepath"
8+
"runtime"
9+
10+
"github.com/itsjoniur/bitlygo/api"
11+
"github.com/itsjoniur/bitlygo/internal/configs"
12+
"github.com/itsjoniur/bitlygo/internal/durable"
13+
"github.com/sirupsen/logrus"
14+
)
15+
16+
var (
17+
configPath string = "../internal/configs/config.yaml"
18+
)
19+
20+
func main() {
21+
// initialize configuration
22+
_, b, _, _ := runtime.Caller(0)
23+
dir := filepath.Dir(path.Join(path.Dir(b)))
24+
25+
configPath = path.Join(dir, configPath)
26+
if err := configs.Init(configPath); err != nil {
27+
log.Panicln(err)
28+
}
29+
30+
configs := configs.AppConfig
31+
// create a database client
32+
db := durable.OpenDatabaseClient(context.Background(), &durable.ConnectionInfo{
33+
User: configs.Database.User,
34+
Password: configs.Database.Password,
35+
Host: configs.Database.Host,
36+
Port: configs.Database.Port,
37+
Name: configs.Database.Name,
38+
})
39+
// create logger
40+
logger := durable.NewLogger(logrus.New())
41+
// serve HTTP
42+
if err := api.StartAPI(logger, db, configs.HTTP.Port); err != nil {
43+
log.Panicln(err)
44+
}
45+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /