first commit

This commit is contained in:
kenneth 2023-12-25 11:08:11 +08:00
parent 4cbf61f542
commit d41908ded5
8 changed files with 286 additions and 0 deletions

3
.gitignore vendored
View File

@ -19,3 +19,6 @@
# Go workspace file # Go workspace file
go.work go.work
.vscode/
.env

18
go.mod Normal file
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
}