├── .example_env ├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd └── api │ ├── error.go │ ├── handler.go │ ├── hash.go │ ├── hash_test.go │ ├── helpers.go │ ├── main.go │ ├── middleware.go │ ├── routers.go │ ├── valid_url.go │ └── valid_url_test.go ├── doc └── img │ └── architecture.png ├── go.mod ├── go.sum ├── internal └── models │ └── url_bank.go ├── tls ├── cert.pem └── key.pem └── tmp ├── error.log └── info.log /.example_env: -------------------------------------------------------------------------------- 1 | DB_URL=postgres://:@:/database_name 2 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go Unit Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | # Add other branches as needed 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.20' 20 | 21 | - name: Test 22 | run: go test -v ./... 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | .env 24 | 25 | .idea 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Md Samiul Islam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-url-shortener 2 | 3 | --- 4 | # Software Architecture 5 | ![image](./doc/img/architecture.png) 6 | 7 | --- 8 | # API list 9 | 10 | ### 1. /api/ping 11 | Request Method: GET 12 | 13 | Response: 14 | ```json 15 | { 16 | "status": "up and running" 17 | } 18 | ``` 19 | 20 | ### 2. /api/shorten 21 | Request Method: POST 22 | 23 | Request Body: 24 | ```json 25 | { 26 | "url": "https://www.youtube.com/watch?v=TLB5MY9BBa4&ab_channel=CoderDave" 27 | } 28 | ``` 29 | Response: 30 | ```json 31 | { 32 | "results": { 33 | "id": 13, 34 | "actual_url": "https://www.youtube.com/watch?v=TLB5MY9BBa4&ab_channel=CoderDave", 35 | "short_url": "77cf66ac", 36 | "total_hit": 0, 37 | "created_at": "2023-07-23T14:37:53.241159Z" 38 | } 39 | } 40 | ``` 41 | 42 | ### 3. /api/decode/{shortUrl} 43 | Request Method: GET 44 | Response: 45 | ```json 46 | { 47 | "results": { 48 | "id": 13, 49 | "actual_url": "https://www.youtube.com/watch?v=TLB5MY9BBa4&ab_channel=CoderDave", 50 | "short_url": "77cf66ac", 51 | "total_hit": 6, 52 | "created_at": "2023-07-23T14:37:53.241159Z" 53 | } 54 | } 55 | ``` 56 | 57 | ### 4. /api/urls?limit=10&offset=10 58 | Request Method: GET 59 | Query Parameter: 60 | ```text 61 | limit = <> // 10 62 | offset = <> // 1 63 | ``` 64 | Response: 65 | ```json 66 | { 67 | "count": 15, 68 | "results": [ 69 | { 70 | "id": 14, 71 | "actual_url": "https://www.example.com/", 72 | "short_url": "8797b6c7", 73 | "total_hit": 0, 74 | "created_at": "2023-07-23T17:16:09.239242Z" 75 | } 76 | ] 77 | } 78 | ``` 79 | 80 | 81 | -------------------------------------------------------------------------------- /cmd/api/error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func (app *application) logError(r *http.Request, err error) { 9 | app.errorLog.Println(err) 10 | } 11 | 12 | func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) { 13 | env := envelope{"error": message} 14 | 15 | err := app.writeJSON(w, status, env, nil) 16 | if err != nil { 17 | app.logError(r, err) 18 | w.WriteHeader(http.StatusInternalServerError) 19 | } 20 | } 21 | 22 | func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) { 23 | app.logError(r, err) 24 | 25 | message := "the server encountered a problem and could not process your request" 26 | app.errorResponse(w, r, http.StatusInternalServerError, message) 27 | } 28 | 29 | func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) { 30 | message := "the requested resource could not be found" 31 | app.errorResponse(w, r, http.StatusNotFound, message) 32 | } 33 | 34 | func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) { 35 | message := fmt.Sprintf("the %s method is not supported for this resource", r.Method) 36 | app.errorResponse(w, r, http.StatusMethodNotAllowed, message) 37 | } 38 | 39 | func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) { 40 | app.errorResponse(w, r, http.StatusBadRequest, err.Error()) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/api/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "github.com/cosmicray001/go-url-shortener/internal/models" 6 | "github.com/julienschmidt/httprouter" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | func (app *application) ping(w http.ResponseWriter, r *http.Request) { 12 | env := envelope{ 13 | "status": "up and running", 14 | } 15 | err := app.writeJSON(w, http.StatusOK, env, nil) 16 | if err != nil { 17 | app.serverErrorResponse(w, r, err) 18 | } 19 | } 20 | 21 | func (app *application) createShortUrl(w http.ResponseWriter, r *http.Request) { 22 | var input struct { 23 | Url string `json:"url"` 24 | } 25 | 26 | err := app.readJSON(w, r, &input) 27 | if err != nil { 28 | app.badRequestResponse(w, r, err) 29 | return 30 | } 31 | if validURL := isValidURL(input.Url); !validURL { 32 | app.badRequestResponse(w, r, errors.New("invalid url")) 33 | return 34 | } 35 | shortUrl, err := app.generateShortUrl(input.Url) 36 | if err != nil { 37 | app.serverErrorResponse(w, r, err) 38 | return 39 | } 40 | var urlBank models.UrlBank 41 | urlBank.ActualUrl = input.Url 42 | urlBank.ShortUrl = shortUrl 43 | err = app.urlBank.Insert(&urlBank) 44 | if err != nil { 45 | app.serverErrorResponse(w, r, err) 46 | return 47 | } 48 | err = app.writeJSON(w, http.StatusCreated, envelope{"results": urlBank}, nil) 49 | if err != nil { 50 | app.serverErrorResponse(w, r, err) 51 | return 52 | } 53 | } 54 | 55 | func (app *application) getLongUrl(w http.ResponseWriter, r *http.Request) { 56 | params := httprouter.ParamsFromContext(r.Context()) 57 | shortUrl := params.ByName("shortUrl") 58 | var urlBank models.UrlBank 59 | urlBank.ShortUrl = shortUrl 60 | err := app.urlBank.UpdateHitCountAndGet(&urlBank) 61 | if err != nil { 62 | app.notFoundResponse(w, r) 63 | return 64 | } 65 | err = app.writeJSON(w, http.StatusOK, envelope{"results": urlBank}, nil) 66 | if err != nil { 67 | app.serverErrorResponse(w, r, err) 68 | return 69 | } 70 | } 71 | 72 | func (app *application) urlList(w http.ResponseWriter, r *http.Request) { 73 | queryValues := r.URL.Query() 74 | limit, err := strconv.Atoi(queryValues.Get("limit")) 75 | if err != nil { 76 | limit = 10 77 | } 78 | offset, err := strconv.Atoi(queryValues.Get("offset")) 79 | if err != nil { 80 | offset = 0 81 | } 82 | 83 | urlBankList, err := app.urlBank.AllUrl(limit, offset) 84 | count, _ := app.urlBank.UrlCount() 85 | response := envelope{ 86 | "count": count, 87 | "results": urlBankList, 88 | } 89 | err = app.writeJSON(w, http.StatusOK, response, nil) 90 | if err != nil { 91 | app.serverErrorResponse(w, r, err) 92 | return 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /cmd/api/hash.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "errors" 7 | "time" 8 | ) 9 | 10 | func (app *application) generateShortUrl(actualUrl string) (string, error) { 11 | for i := 0; i < 5; i++ { 12 | text := actualUrl + time.Now().String() 13 | hashedStr := GetMD5Hash(text)[:8] 14 | ok, err := app.urlBank.CheckExistUrl(hashedStr) 15 | if err != nil { 16 | return "", err 17 | } 18 | if !ok { 19 | return hashedStr, nil 20 | } 21 | } 22 | return "", errors.New("limit finished") 23 | } 24 | 25 | func GetMD5Hash(text string) string { 26 | if text == "" { 27 | return "" 28 | } 29 | hasher := md5.New() 30 | hasher.Write([]byte(text)) 31 | return hex.EncodeToString(hasher.Sum(nil)) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/api/hash_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetMD5Hash(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | text string 11 | want string 12 | }{ 13 | { 14 | name: "Normal URL", 15 | text: "www.example.com", 16 | want: "7c1767b30512b6003fd3c2e618a86522", 17 | }, 18 | { 19 | name: "Empty URL", 20 | text: "", 21 | want: "", 22 | }, 23 | } 24 | for _, tt := range tests { 25 | t.Run(tt.name, func(t *testing.T) { 26 | hashText := GetMD5Hash(tt.text) 27 | if tt.want != hashText { 28 | t.Errorf("want:%s got: %s", tt.want, hashText) 29 | } 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cmd/api/helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | type envelope map[string]interface{} 12 | 13 | func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error { 14 | js, err := json.Marshal(data) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | js = append(js, '\n') 20 | 21 | for key, value := range headers { 22 | w.Header()[key] = value 23 | } 24 | 25 | w.Header().Set("Content-Type", "application/json") 26 | w.WriteHeader(status) 27 | _, _ = w.Write(js) 28 | 29 | return nil 30 | } 31 | 32 | func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error { 33 | err := json.NewDecoder(r.Body).Decode(dst) 34 | if err != nil { 35 | var syntaxError *json.SyntaxError 36 | var unmarshalTypeError *json.UnmarshalTypeError 37 | var invalidUnmarshalError *json.InvalidUnmarshalError 38 | switch { 39 | case errors.As(err, &syntaxError): 40 | return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset) 41 | case errors.Is(err, io.ErrUnexpectedEOF): 42 | return errors.New("body contains badly-formed JSON") 43 | case errors.As(err, &unmarshalTypeError): 44 | if unmarshalTypeError.Field != "" { 45 | return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field) 46 | } 47 | return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset) 48 | case errors.As(err, &invalidUnmarshalError): 49 | panic(err) 50 | default: 51 | return err 52 | } 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "database/sql" 7 | "flag" 8 | "github.com/cosmicray001/go-url-shortener/internal/models" 9 | "github.com/joho/godotenv" 10 | _ "github.com/lib/pq" 11 | "log" 12 | "net/http" 13 | "os" 14 | "time" 15 | ) 16 | 17 | type application struct { 18 | errorLog *log.Logger 19 | infoLog *log.Logger 20 | urlBank models.UrlBankModel 21 | } 22 | 23 | func main() { 24 | addr := flag.String("addr", ":8000", "HTTP network address") 25 | err := godotenv.Load() 26 | if err != nil { 27 | log.Fatal("Error loading .env file") 28 | } 29 | dbString := os.Getenv("DB_URL") 30 | db, err := openDB(dbString) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | defer db.Close() 35 | flag.Parse() 36 | 37 | infoLogFile, err := os.OpenFile("./tmp/info.log", os.O_RDWR|os.O_CREATE, 0666) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | defer infoLogFile.Close() 42 | infoLog := log.New(infoLogFile, "INFO\t", log.Ldate|log.Ltime) 43 | 44 | errorLogFile, err := os.OpenFile("./tmp/error.log", os.O_RDWR|os.O_CREATE, 0666) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | defer errorLogFile.Close() 49 | errorLog := log.New(errorLogFile, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) 50 | 51 | app := &application{ 52 | errorLog: errorLog, 53 | infoLog: infoLog, 54 | urlBank: models.UrlBankModel{ 55 | DB: db, 56 | }, 57 | } 58 | 59 | tlsConfig := &tls.Config{ 60 | CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, 61 | } 62 | 63 | srv := &http.Server{ 64 | Addr: *addr, 65 | ErrorLog: errorLog, 66 | Handler: app.routers(), 67 | TLSConfig: tlsConfig, 68 | IdleTimeout: time.Minute, 69 | ReadTimeout: 5 * time.Second, 70 | WriteTimeout: 10 * time.Second, 71 | } 72 | infoLog.Printf("Starting server on : %s\n", *addr) 73 | err = srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem") 74 | if err != nil { 75 | errorLog.Fatal(err) 76 | } 77 | } 78 | 79 | func openDB(dsn string) (*sql.DB, error) { 80 | db, err := sql.Open("postgres", dsn) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 86 | defer cancel() 87 | 88 | err = db.PingContext(ctx) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return db, nil 93 | } 94 | -------------------------------------------------------------------------------- /cmd/api/middleware.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | func (app *application) logRequest(next http.Handler) http.Handler { 9 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 10 | startTime := time.Now() 11 | next.ServeHTTP(w, r) 12 | duration := time.Now().Sub(startTime) 13 | app.infoLog.Printf("%s - %s %s %s time takes: %v", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI(), duration) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /cmd/api/routers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/julienschmidt/httprouter" 5 | "net/http" 6 | ) 7 | 8 | func (app *application) routers() http.Handler { 9 | router := httprouter.New() 10 | 11 | router.NotFound = http.HandlerFunc(app.notFoundResponse) 12 | 13 | router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) 14 | 15 | router.HandlerFunc(http.MethodGet, "/api/ping", app.ping) 16 | router.HandlerFunc(http.MethodPost, "/api/shorten", app.createShortUrl) 17 | router.HandlerFunc(http.MethodGet, "/api/decode/:shortUrl", app.getLongUrl) 18 | router.HandlerFunc(http.MethodGet, "/api/urls", app.urlList) 19 | 20 | return app.logRequest(router) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/api/valid_url.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func isValidURL(inputURL string) bool { 8 | if inputURL == "" { 9 | return false 10 | } 11 | 12 | resp, err := http.Head(inputURL) 13 | if err != nil { 14 | return false 15 | } 16 | defer resp.Body.Close() 17 | 18 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 19 | return true 20 | } 21 | 22 | return false 23 | } 24 | -------------------------------------------------------------------------------- /cmd/api/valid_url_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIsValidURL(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | inputUrl string 11 | want bool 12 | }{ 13 | // Valid URLs with 2xx status codes 14 | {"Valid URL", "https://www.google.com", true}, 15 | {"Valid URL", "https://github.com/cosmicray001", true}, 16 | {"Valid URL", "https://www.example.com", true}, 17 | 18 | // Invalid URLs with non-2xx status codes 19 | {"Invalid URL", "https://www.nonexistent-link.com", false}, 20 | {"Invalid URL", "https://www.invalid-link.com", false}, 21 | 22 | // Empty inputURL should return false 23 | {"empty URL", "", false}, 24 | } 25 | 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | result := isValidURL(tt.inputUrl) 29 | if tt.want != result { 30 | t.Errorf("want:%v got: %v", tt.want, result) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /doc/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicray001/go-url-shortener/32af5a0d04db61277da4b58a72529977313b64c1/doc/img/architecture.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cosmicray001/go-url-shortener 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/joho/godotenv v1.5.1 7 | github.com/julienschmidt/httprouter v1.3.0 8 | github.com/lib/pq v1.10.9 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 2 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 3 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 4 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 5 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 6 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 7 | -------------------------------------------------------------------------------- /internal/models/url_bank.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | type UrlBank struct { 10 | ID int64 `json:"id"` 11 | ActualUrl string `json:"actual_url"` 12 | ShortUrl string `json:"short_url"` 13 | TotalHit int64 `json:"total_hit"` 14 | CreatedAt time.Time `json:"created_at"` 15 | } 16 | 17 | type UrlBankModel struct { 18 | DB *sql.DB 19 | } 20 | 21 | func (m *UrlBankModel) CheckExistUrl(shortUrl string) (bool, error) { 22 | var exists bool 23 | 24 | query := "SELECT EXISTS(SELECT true FROM url_bank WHERE short_url = $1)" 25 | 26 | err := m.DB.QueryRow(query, shortUrl).Scan(&exists) 27 | 28 | return exists, err 29 | } 30 | 31 | func (m *UrlBankModel) Insert(urlBank *UrlBank) error { 32 | query := `INSERT INTO url_bank (actual_url, short_url, total_hit, created_at) VALUES($1, $2, $3, $4) RETURNING id, created_at` 33 | args := []interface{}{urlBank.ActualUrl, urlBank.ShortUrl, 0, time.Now().UTC()} 34 | err := m.DB.QueryRow(query, args...).Scan(&urlBank.ID, &urlBank.CreatedAt) 35 | return err 36 | } 37 | 38 | func (m *UrlBankModel) Get(shortUrl string) (*UrlBank, error) { 39 | var urlBank UrlBank 40 | query := `SELECT id, actual_url, short_url, total_hit, created_at FROM url_bank WHERE short_url = $1` 41 | err := m.DB.QueryRow(query, shortUrl).Scan( 42 | &urlBank.ID, 43 | &urlBank.ActualUrl, 44 | &urlBank.ShortUrl, 45 | &urlBank.TotalHit, 46 | &urlBank.CreatedAt, 47 | ) 48 | 49 | if err != nil { 50 | switch { 51 | case errors.Is(err, sql.ErrNoRows): 52 | return nil, errors.New("models: no matching record found") 53 | default: 54 | return nil, err 55 | } 56 | } 57 | return &urlBank, nil 58 | } 59 | 60 | func (m *UrlBankModel) UpdateHitCountAndGet(urlBank *UrlBank) error { 61 | query := `UPDATE url_bank SET total_hit = total_hit + 1 WHERE short_url = $1 62 | RETURNING id, actual_url, short_url, total_hit, created_at` 63 | args := []interface{}{ 64 | urlBank.ShortUrl, 65 | } 66 | return m.DB.QueryRow(query, args...).Scan( 67 | &urlBank.ID, 68 | &urlBank.ActualUrl, 69 | &urlBank.ShortUrl, 70 | &urlBank.TotalHit, 71 | &urlBank.CreatedAt, 72 | ) 73 | } 74 | 75 | func (m *UrlBankModel) AllUrl(limit, offset int) (*[]UrlBank, error) { 76 | var urlBankList []UrlBank 77 | query := `SELECT * FROM url_bank ORDER BY Id DESC LIMIT $1 OFFSET $2` 78 | rows, err := m.DB.Query(query, limit, offset) 79 | if err != nil { 80 | return nil, err 81 | } 82 | for rows.Next() { 83 | var urlBank UrlBank 84 | rows.Scan(&urlBank.ID, &urlBank.ActualUrl, &urlBank.ShortUrl, &urlBank.TotalHit, &urlBank.CreatedAt) 85 | urlBankList = append(urlBankList, urlBank) 86 | } 87 | return &urlBankList, nil 88 | } 89 | 90 | func (m *UrlBankModel) UrlCount() (int, error) { 91 | var count int 92 | query := `SELECT COUNT(*) FROM url_bank` 93 | err := m.DB.QueryRow(query).Scan(&count) 94 | if err != nil { 95 | return 0, err 96 | } 97 | return count, nil 98 | } 99 | -------------------------------------------------------------------------------- /tls/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+TCCAeGgAwIBAgIQN3P7FVUayQAWB1R5eIe9ZzANBgkqhkiG9w0BAQsFADAS 3 | MRAwDgYDVQQKEwdBY21lIENvMB4XDTIzMDYyNzE3MzMyMFoXDTI0MDYyNjE3MzMy 4 | MFowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 5 | AQoCggEBANs46HHsrT8N0TU8nR9nhPAQ9Uev8OgrTMk6O4lxquQ36ORyCRBFOgpL 6 | OK2PNRTrAofhPMGTNqn/IKdTySGUP3JZGRJJ3Tr5J+v0p8jve133bIR1tiF5NSk8 7 | snM7qgjLGAyLLvCnh0Nl+F0+FpWe+oRcvJZQBWAr6JUah6JdfzM9sh//yhLBub6P 8 | XfzLjCHFNwewFJ+CSlcub0ZvG69udtiGoPx5ePM2HeHS4OuKy6B0NEAfmaPXzj6H 9 | 6c//Wi5sUU/iCn8HH0PeHfN8F5vSCrQ+tADQpMIjpqW5qmC3e4wiWJ8J/amgzwLn 10 | rS/AXoOoY0kzN4SNq8K8VYrBAsBgYp8CAwEAAaNLMEkwDgYDVR0PAQH/BAQDAgWg 11 | MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJ 12 | bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQDZHPd3DABCYsFCabJYcOUMCm8b 13 | Aqr4UJa5rRbXFUry3ADFkWKJLBYAFMXOAN61O4z39+Tl/FbrQMvgooZdXv8Q51Ol 14 | Fs+EzfbGDnLgfTSEI9sB/0J36cCsf5v4wjRStyAXIiykYn/qvgvM+vk++0+Ze811 15 | EHKrAbYzGwmtsLNy20G9KMZvFITQ486K/4ayIamEgl8LWEP2RxcCBnycGbMWHJGx 16 | 7aAY/vJhwpAquRLZ+am1NVZyhs/uONlcMrCe5d3cbVKGyzwHWKgNXtmoL/FoE9HB 17 | 7bDVRNH24cIsc/RfprdNLXC3M6M4EzsysuPMXl03UWzo93/dx6ILAF+vk1Mo 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /tls/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDbOOhx7K0/DdE1 3 | PJ0fZ4TwEPVHr/DoK0zJOjuJcarkN+jkcgkQRToKSzitjzUU6wKH4TzBkzap/yCn 4 | U8khlD9yWRkSSd06+Sfr9KfI73td92yEdbYheTUpPLJzO6oIyxgMiy7wp4dDZfhd 5 | PhaVnvqEXLyWUAVgK+iVGoeiXX8zPbIf/8oSwbm+j138y4whxTcHsBSfgkpXLm9G 6 | bxuvbnbYhqD8eXjzNh3h0uDrisugdDRAH5mj184+h+nP/1oubFFP4gp/Bx9D3h3z 7 | fBeb0gq0PrQA0KTCI6aluapgt3uMIlifCf2poM8C560vwF6DqGNJMzeEjavCvFWK 8 | wQLAYGKfAgMBAAECggEBAIq8CgbHFKDZ2rNJR9pvBedzo/aZNumHNZIHo1QJNnA1 9 | ndKtBGVimieXkSftgFdbnv9lILEUvDx7NCwQzzHMAxcXodE8I6DFu2kNLP3x6O3/ 10 | vpJZeEjYzzborbpVYPctqGuPzcYJD4YsyzAfMeXAdaQ8mDC8NZq2TvvFjV7Iss42 11 | qYprJhcEzBPI5UO/YkYpcfGkCfbSv6pohRpeCY6G7TyBgx466GncJZeX8jot+6nq 12 | ybBSzBOZkBEcOpSa/HpH7iGTFeFLw3K6LvZA6zj5a3BbsxmdZ+20zP690dVkVplq 13 | 1WeqbPQbmu1zqKpFTaPglU96a8IDV07fp8/PPFU8uekCgYEA/1TEkGbYz3Kz42uy 14 | jONHZlMx1zVHU9Auz+MMe67rk0rML8S6eQ3mZIc9zuvjSF8olf6wQJ7jGCx1PwFL 15 | oU8RDR6hgQvheU/CbPtufX59Yajp0jx74SbBNnr97S39O5+zy8BB2c3DuSqI10RB 16 | HP9tTbJjvq/dIwfJRKXMkt/fXPsCgYEA28vssNdD8fC43RrmA1CEPwk6U9cR5SHv 17 | 0XDb1S+fRvUUlH0H8ndPS5w8FxcQn5SufwXJwit3un326wUVYATs45wenFKXAxLL 18 | HNQuD0eTpzmLHasFa7lHcrgNKvqhu6aTgWevv8bAJiCrlRqix1IPadqvCNxl9qQI 19 | F1K33Dr8F60CgYEAqOAWCjQb3klNNXO76ndAeCs6Yc65WHPvIkxXq05sPUVRe56w 20 | fvR0l7TjaoKWFv3pSBvl0zfDl+9/tKZUsWIsMvM5erKy9JuOSqqZz6Ljpr4juIuM 21 | m8QYMsJyRHcQcGkWcAK/CyGO/F9nlolsT5OZZbRBSJPkvRCfTCok/lfX0hUCgYAZ 22 | plKY0IY9Vbo9E0aEXIESWiFUZ2TPOIakCmedGVGdFjywl2a2BPZb/H+GskpeVRuH 23 | 90T0Q95VBR4rjwxPiEOczDtGQt0cnrTVSG2XmuLAQRba/3pCd/y+hnwC5/y3yVit 24 | YUxBNRB3YpijbdhRKmJiGAMVJaNAhSPslNqf+MIygQKBgEWzAxXXCO8/edn5y43O 25 | yiFI/c0FpyUnCKfEqnsXZBwaxpgyi0vTRqrikOtBznSLrhxMNASxgAgTTs+ZGUcL 26 | moWwkM6KnGJ0zDvz+J6N9L+sBunovdB+JX4CXH8YDob1r2rkfm9mu+ZclRmoqNNJ 27 | ZEMGSd+8p8deQwJUayvF3Pse 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tmp/error.log: -------------------------------------------------------------------------------- 1 | ERROR 2023/07/23 23:27:39 server.go:3215: http: TLS handshake error from 127.0.0.1:41922: remote error: tls: unknown certificate 2 | -------------------------------------------------------------------------------- /tmp/info.log: -------------------------------------------------------------------------------- 1 | INFO 2023/07/23 23:27:20 Starting server on : :8000 2 | INFO 2023/07/23 23:27:44 127.0.0.1:41932 - HTTP/2.0 POST /api/shorten time takes: 5.309294117s 3 | INFO 2023/07/23 23:28:28 127.0.0.1:41932 - HTTP/2.0 POST /api/shorten time takes: 20.500062ms 4 | --------------------------------------------------------------------------------