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
	 kenneth
						kenneth