first commit
This commit is contained in:
parent
4cbf61f542
commit
d41908ded5
3
.gitignore
vendored
3
.gitignore
vendored
@ -19,3 +19,6 @@
|
|||||||
|
|
||||||
# Go workspace file
|
# Go workspace file
|
||||||
go.work
|
go.work
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
.env
|
||||||
18
go.mod
Normal file
18
go.mod
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module github.com/zhang2092/go-url-shortener
|
||||||
|
|
||||||
|
go 1.21.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/mux v1.8.1
|
||||||
|
github.com/itchyny/base58-go v0.2.1
|
||||||
|
github.com/redis/go-redis/v9 v9.3.1
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
24
go.sum
Normal file
24
go.sum
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/itchyny/base58-go v0.2.1 h1:wtnhAVdOcW3WuHEASmGHMms4juOB8yEpj/KJxlB57+k=
|
||||||
|
github.com/itchyny/base58-go v0.2.1/go.mod h1:BNvrKeAtWNSca1GohNbyhfff9/v0IrZjzWCAGeAvZZE=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
|
||||||
|
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
74
handler/handlers.go
Normal file
74
handler/handlers.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/zhang2092/go-url-shortener/shortener"
|
||||||
|
"github.com/zhang2092/go-url-shortener/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UrlCreationRequest struct {
|
||||||
|
LongUrl string `json:"long_url"`
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UrlCreationResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
ShortUrl string `json:"short_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateShortUrl(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var req UrlCreationRequest
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid parameter", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shortUrl, err := shortener.GenerateShortLink(req.LongUrl, req.UserId)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to generate short link", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.SaveUrlMapping(shortUrl, req.LongUrl, req.UserId)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to store url mapping", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := "http://"
|
||||||
|
if r.TLS != nil {
|
||||||
|
scheme = "https://"
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &UrlCreationResponse{
|
||||||
|
Message: "short url created successfully",
|
||||||
|
ShortUrl: scheme + r.Host + "/" + shortUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleShortUrlRedirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
shorUrl := vars["shortUrl"]
|
||||||
|
link, err := store.RetrieveInitialUrl(shorUrl)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to get url", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, link, http.StatusFound)
|
||||||
|
}
|
||||||
52
main.go
Normal file
52
main.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/zhang2092/go-url-shortener/handler"
|
||||||
|
"github.com/zhang2092/go-url-shortener/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Wecome to the URL Shortener API"))
|
||||||
|
}).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
router.HandleFunc("/create-short-url", handler.CreateShortUrl).Methods(http.MethodPost)
|
||||||
|
router.HandleFunc("/{shortUrl}", handler.HandleShortUrlRedirect).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
store.InitializeStore()
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: "0.0.0.0:9090",
|
||||||
|
Handler: router,
|
||||||
|
WriteTimeout: 15 * time.Second,
|
||||||
|
ReadTimeout: 15 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
|
log.Printf("failed to start server on :9000, err: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt)
|
||||||
|
<-c
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
store.CloseStoreRedisConn()
|
||||||
|
|
||||||
|
srv.Shutdown(ctx)
|
||||||
|
log.Println("shutting down")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
35
shortener/shortener.go
Normal file
35
shortener/shortener.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package shortener
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/itchyny/base58-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sha256Of(input string) []byte {
|
||||||
|
algorithm := sha256.New()
|
||||||
|
algorithm.Write([]byte(input))
|
||||||
|
return algorithm.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func base58Encoded(bytes []byte) (string, error) {
|
||||||
|
encoded, err := base58.BitcoinEncoding.Encode(bytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(encoded), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateShortLink(originUrl string, userId string) (string, error) {
|
||||||
|
urlHashByte := sha256Of(originUrl + userId)
|
||||||
|
generateNumber := new(big.Int).SetBytes(urlHashByte).Uint64()
|
||||||
|
result, err := base58Encoded([]byte(fmt.Sprintf("%d", generateNumber)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
26
shortener/shortener_test.go
Normal file
26
shortener/shortener_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package shortener
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const userId = "7c729139-7ff4-445f-976b-2d842f55cb0e"
|
||||||
|
|
||||||
|
func TestGenerateShortLink(t *testing.T) {
|
||||||
|
link1 := "https://www.baidu.com/"
|
||||||
|
short1, err := GenerateShortLink(link1, userId)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, short1, "egtq236P5f3")
|
||||||
|
|
||||||
|
link2 := "https://www.163.com/"
|
||||||
|
short2, err := GenerateShortLink(link2, userId)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, short2, "DiCqg9YpV89")
|
||||||
|
|
||||||
|
link3 := "https://www.qq.com/"
|
||||||
|
short3, err := GenerateShortLink(link3, userId)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, short3, "4QhQ62cZem1")
|
||||||
|
}
|
||||||
54
store/store.go
Normal file
54
store/store.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StorageService struct {
|
||||||
|
redisClient *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
storeService = &StorageService{}
|
||||||
|
ctx = context.Background()
|
||||||
|
)
|
||||||
|
|
||||||
|
const CacheDuration = 6 * time.Hour
|
||||||
|
|
||||||
|
func InitializeStore() {
|
||||||
|
redisClient := redis.NewClient(&redis.Options{
|
||||||
|
Addr: "localhost:6378",
|
||||||
|
Password: "secret",
|
||||||
|
DB: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
pong, err := redisClient.Ping(ctx).Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to init redis: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Redis started successfully, ping message = %s\n", pong)
|
||||||
|
storeService.redisClient = redisClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseStoreRedisConn() {
|
||||||
|
storeService.redisClient.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveUrlMapping(shortUrl string, originUrl string, userId string) error {
|
||||||
|
return storeService.redisClient.Set(ctx, shortUrl, originUrl, CacheDuration).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RetrieveInitialUrl(shortUrl string) (string, error) {
|
||||||
|
result, err := storeService.redisClient.Get(ctx, shortUrl).Result()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user