Use gofmt compliant formatting (#8192)
This commit is contained in:
1 parent
ed8c2f3168
commit
32690e98da
3 files changed
+557
-556
No files matched your search
@@ -104,16 +104,16 @@ Go and Java are powerful languages with distinct approaches to handling errors.
|
||||
|
||||
```go
|
||||
func divide(a, b int) (int, error) {
|
||||
if b == 0 {
|
||||
return 0, errors.New("division by zero")
|
||||
}
|
||||
return a / b, nil
|
||||
if b == 0 {
|
||||
return 0, errors.New("division by zero")
|
||||
}
|
||||
return a / b, nil
|
||||
}
|
||||
|
||||
// Usage
|
||||
result, err := divide(10, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+339
-338
@@ -105,26 +105,26 @@ package api
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type Book struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Year int `json:"year"`
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Year int `json:"year"`
|
||||
}
|
||||
|
||||
type JsonResponse struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
func ResponseJSON(c *gin.Context, status int, message string, data any) {
|
||||
response := JsonResponse{
|
||||
Status: status,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
response := JsonResponse{
|
||||
Status: status,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
c.JSON(status, response)
|
||||
c.JSON(status, response)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -140,45 +140,45 @@ To create a new book handler function, create an `api/handlers.go` file that wil
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func InitDB() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
|
||||
dsn := os.Getenv("DB_URL")
|
||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
dsn := os.Getenv("DB_URL")
|
||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
|
||||
// migrate the schema
|
||||
if err := DB.AutoMigrate(&Book{}); err != nil {
|
||||
log.Fatal("Failed to migrate schema:", err)
|
||||
}
|
||||
// migrate the schema
|
||||
if err := DB.AutoMigrate(&Book{}); err != nil {
|
||||
log.Fatal("Failed to migrate schema:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateBook(c *gin.Context) {
|
||||
var book Book
|
||||
var book Book
|
||||
|
||||
//bind the request body
|
||||
if err := c.ShouldBindJSON(&book); err != nil {
|
||||
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
|
||||
return
|
||||
}
|
||||
DB.Create(&book)
|
||||
ResponseJSON(c, http.StatusCreated, "Book created successfully", book)
|
||||
//bind the request body
|
||||
if err := c.ShouldBindJSON(&book); err != nil {
|
||||
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
|
||||
return
|
||||
}
|
||||
DB.Create(&book)
|
||||
ResponseJSON(c, http.StatusCreated, "Book created successfully", book)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -192,9 +192,9 @@ To retrieve the list of books, create a `GetBooks` handler that uses the `DB` va
|
||||
|
||||
```go
|
||||
func GetBooks(c *gin.Context) {
|
||||
var books []Book
|
||||
DB.Find(&books)
|
||||
ResponseJSON(c, http.StatusOK, "Books retrieved successfully", books)
|
||||
var books []Book
|
||||
DB.Find(&books)
|
||||
ResponseJSON(c, http.StatusOK, "Books retrieved successfully", books)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -204,12 +204,12 @@ To retrieve a book, create a `GetBook` handler that uses the `DB` variable to ge
|
||||
|
||||
```go
|
||||
func GetBook(c *gin.Context) {
|
||||
var book Book
|
||||
if err := DB.First(&book, c.Param("id")).Error; err != nil {
|
||||
ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
|
||||
return
|
||||
}
|
||||
ResponseJSON(c, http.StatusOK, "Book retrieved successfully", book)
|
||||
var book Book
|
||||
if err := DB.First(&book, c.Param("id")).Error; err != nil {
|
||||
ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
|
||||
return
|
||||
}
|
||||
ResponseJSON(c, http.StatusOK, "Book retrieved successfully", book)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -219,20 +219,20 @@ To update a book, create an `UpdateBook` handler that uses the `DB` variable to
|
||||
|
||||
```go
|
||||
func UpdateBook(c *gin.Context) {
|
||||
var book Book
|
||||
if err := DB.First(&book, c.Param("id")).Error; err != nil {
|
||||
ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
|
||||
return
|
||||
}
|
||||
var book Book
|
||||
if err := DB.First(&book, c.Param("id")).Error; err != nil {
|
||||
ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// bind the request body
|
||||
if err := c.ShouldBindJSON(&book); err != nil {
|
||||
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
|
||||
return
|
||||
}
|
||||
// bind the request body
|
||||
if err := c.ShouldBindJSON(&book); err != nil {
|
||||
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
|
||||
return
|
||||
}
|
||||
|
||||
DB.Save(&book)
|
||||
ResponseJSON(c, http.StatusOK, "Book updated successfully", book)
|
||||
DB.Save(&book)
|
||||
ResponseJSON(c, http.StatusOK, "Book updated successfully", book)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -242,13 +242,13 @@ To delete a book, create a `DeleteBook` handler that uses the `DB` variable to g
|
||||
|
||||
```go
|
||||
func DeleteBook(c *gin.Context) {
|
||||
var book Book
|
||||
if err := DB.Delete(&book, c.Param("id")).Error; err != nil {
|
||||
ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
|
||||
return
|
||||
}
|
||||
ResponseJSON(c, http.StatusOK, "Book deleted successfully", nil)
|
||||
}
|
||||
var book Book
|
||||
if err := DB.Delete(&book, c.Param("id")).Error; err != nil {
|
||||
ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
|
||||
return
|
||||
}
|
||||
ResponseJSON(c, http.StatusOK, "Book deleted successfully", nil)
|
||||
}
|
||||
```
|
||||
|
||||
### Putting it all together
|
||||
@@ -259,22 +259,22 @@ With the handlers set up, you need to create the application entry point and spe
|
||||
package main
|
||||
|
||||
import (
|
||||
"go_book_api/api"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go_book_api/api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
api.InitDB()
|
||||
r := gin.Default()
|
||||
api.InitDB()
|
||||
r := gin.Default()
|
||||
|
||||
//routes
|
||||
r.POST("/book", api.CreateBook)
|
||||
r.GET("/books", api.GetBooks)
|
||||
r.GET("/book/:id", api.GetBook)
|
||||
r.PUT("/book/:id", api.UpdateBook)
|
||||
r.DELETE("/book/:id", api.DeleteBook)
|
||||
//routes
|
||||
r.POST("/book", api.CreateBook)
|
||||
r.GET("/books", api.GetBooks)
|
||||
r.GET("/book/:id", api.GetBook)
|
||||
r.PUT("/book/:id", api.UpdateBook)
|
||||
r.DELETE("/book/:id", api.DeleteBook)
|
||||
|
||||
r.Run(":8080")
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -366,31 +366,31 @@ Go comes with a testing package that makes it easy to write unit tests, integrat
|
||||
package tests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"go_book_api/api"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go_book_api/api"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupTestDB() {
|
||||
var err error
|
||||
api.DB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect test database")
|
||||
}
|
||||
api.DB.AutoMigrate(&api.Book{})
|
||||
var err error
|
||||
api.DB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect test database")
|
||||
}
|
||||
api.DB.AutoMigrate(&api.Book{})
|
||||
}
|
||||
|
||||
func addBook() api.Book {
|
||||
book := api.Book{Title: "Go Programming", Author: "John Doe", Year: 2023}
|
||||
api.DB.Create(&book)
|
||||
return book
|
||||
book := api.Book{Title: "Go Programming", Author: "John Doe", Year: 2023}
|
||||
api.DB.Create(&book)
|
||||
return book
|
||||
}
|
||||
```
|
||||
|
||||
@@ -404,29 +404,29 @@ Create a `TestCreateBook` test function that sets up a mock HTTP server using Gi
|
||||
|
||||
```go
|
||||
func TestCreateBook(t *testing.T) {
|
||||
setupTestDB()
|
||||
router := gin.Default()
|
||||
router.POST("/book", api.CreateBook)
|
||||
setupTestDB()
|
||||
router := gin.Default()
|
||||
router.POST("/book", api.CreateBook)
|
||||
|
||||
book := api.Book{
|
||||
Title: "Demo Book name", Author: "Demo Author name", Year: 2021,
|
||||
}
|
||||
book := api.Book{
|
||||
Title: "Demo Book name", Author: "Demo Author name", Year: 2021,
|
||||
}
|
||||
|
||||
jsonValue, _ := json.Marshal(book)
|
||||
req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue))
|
||||
jsonValue, _ := json.Marshal(book)
|
||||
req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if status := w.Code; status != http.StatusCreated {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusCreated, status)
|
||||
}
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
if status := w.Code; status != http.StatusCreated {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusCreated, status)
|
||||
}
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
|
||||
if response.Data == nil {
|
||||
t.Errorf("Expected book data, got nil")
|
||||
}
|
||||
if response.Data == nil {
|
||||
t.Errorf("Expected book data, got nil")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -436,25 +436,25 @@ Similar to the `TestCreateBook` test function, create a `TestGetBooks` that test
|
||||
|
||||
```go
|
||||
func TestGetBooks(t *testing.T) {
|
||||
setupTestDB()
|
||||
addBook()
|
||||
router := gin.Default()
|
||||
router.GET("/books", api.GetBooks)
|
||||
setupTestDB()
|
||||
addBook()
|
||||
router := gin.Default()
|
||||
router.GET("/books", api.GetBooks)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/books", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
req, _ := http.NewRequest("GET", "/books", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
|
||||
if len(response.Data.([]interface{})) == 0 {
|
||||
t.Errorf("Expected non-empty books list")
|
||||
}
|
||||
if len(response.Data.([]interface{})) == 0 {
|
||||
t.Errorf("Expected non-empty books list")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -464,25 +464,25 @@ Create a `TestGetBook` test function that tests the `GetBook` handler functional
|
||||
|
||||
```go
|
||||
func TestGetBook(t *testing.T) {
|
||||
setupTestDB()
|
||||
book := addBook()
|
||||
router := gin.Default()
|
||||
router.GET("/book/:id", api.GetBook)
|
||||
setupTestDB()
|
||||
book := addBook()
|
||||
router := gin.Default()
|
||||
router.GET("/book/:id", api.GetBook)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/book/"+strconv.Itoa(int(book.ID)), nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
req, _ := http.NewRequest("GET", "/book/"+strconv.Itoa(int(book.ID)), nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
|
||||
if response.Data == nil || response.Data.(map[string]interface{})["id"] != float64(book.ID) {
|
||||
t.Errorf("Expected book ID %d, got nil or wrong ID", book.ID)
|
||||
}
|
||||
if response.Data == nil || response.Data.(map[string]interface{})["id"] != float64(book.ID) {
|
||||
t.Errorf("Expected book ID %d, got nil or wrong ID", book.ID)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -492,30 +492,30 @@ Create a `TestUpdateBook` test function that tests the `UpdateBook` handler func
|
||||
|
||||
```go
|
||||
func TestUpdateBook(t *testing.T) {
|
||||
setupTestDB()
|
||||
book := addBook()
|
||||
router := gin.Default()
|
||||
router.PUT("/book/:id", api.UpdateBook)
|
||||
setupTestDB()
|
||||
book := addBook()
|
||||
router := gin.Default()
|
||||
router.PUT("/book/:id", api.UpdateBook)
|
||||
|
||||
updateBook := api.Book{
|
||||
Title: "Advanced Go Programming", Author: "Demo Author name", Year: 2021,
|
||||
}
|
||||
jsonValue, _ := json.Marshal(updateBook)
|
||||
updateBook := api.Book{
|
||||
Title: "Advanced Go Programming", Author: "Demo Author name", Year: 2021,
|
||||
}
|
||||
jsonValue, _ := json.Marshal(updateBook)
|
||||
|
||||
req, _ := http.NewRequest("PUT", "/book/"+strconv.Itoa(int(book.ID)), bytes.NewBuffer(jsonValue))
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
req, _ := http.NewRequest("PUT", "/book/"+strconv.Itoa(int(book.ID)), bytes.NewBuffer(jsonValue))
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
|
||||
if response.Data == nil || response.Data.(map[string]interface{})["title"] != "Advanced Go Programming" {
|
||||
t.Errorf("Expected updated book title 'Advanced Go Programming', got %v", response.Data)
|
||||
}
|
||||
if response.Data == nil || response.Data.(map[string]interface{})["title"] != "Advanced Go Programming" {
|
||||
t.Errorf("Expected updated book title 'Advanced Go Programming', got %v", response.Data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -525,32 +525,32 @@ Create a `TestDeleteBook` test function that tests the `DeleteBook` handler func
|
||||
|
||||
```go
|
||||
func TestDeleteBook(t *testing.T) {
|
||||
setupTestDB()
|
||||
book := addBook()
|
||||
router := gin.Default()
|
||||
router.DELETE("/book/:id", api.DeleteBook)
|
||||
setupTestDB()
|
||||
book := addBook()
|
||||
router := gin.Default()
|
||||
router.DELETE("/book/:id", api.DeleteBook)
|
||||
|
||||
req, _ := http.NewRequest("DELETE", "/book/"+strconv.Itoa(int(book.ID)), nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
req, _ := http.NewRequest("DELETE", "/book/"+strconv.Itoa(int(book.ID)), nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
|
||||
if response.Message != "Book deleted successfully" {
|
||||
t.Errorf("Expected delete message 'Book deleted successfully', got %v", response.Message)
|
||||
}
|
||||
if response.Message != "Book deleted successfully" {
|
||||
t.Errorf("Expected delete message 'Book deleted successfully', got %v", response.Message)
|
||||
}
|
||||
|
||||
//verify that the book was deleted
|
||||
var deletedBook api.Book
|
||||
result := api.DB.First(&deletedBook, book.ID)
|
||||
if result.Error == nil {
|
||||
t.Errorf("Expected book to be deleted, but it still exists")
|
||||
}
|
||||
//verify that the book was deleted
|
||||
var deletedBook api.Book
|
||||
result := api.DB.First(&deletedBook, book.ID)
|
||||
if result.Error == nil {
|
||||
t.Errorf("Expected book to be deleted, but it still exists")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -604,40 +604,40 @@ Next, create a `middleware.go` file inside the `api` folder and add the snippet
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Secret key for signing JWT
|
||||
var jwtSecret = []byte(os.Getenv("SECRET_TOKEN"))
|
||||
|
||||
func JWTAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tokenString := c.GetHeader("Authorization")
|
||||
if tokenString == "" {
|
||||
ResponseJSON(c, http.StatusUnauthorized, "Authorization token required", nil)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// parse and validate the token
|
||||
_, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
// Validate the signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtSecret, nil
|
||||
})
|
||||
if err != nil {
|
||||
ResponseJSON(c, http.StatusUnauthorized, "Invalid token", nil)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// Token is valid, proceed to the next handler
|
||||
c.Next()
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
tokenString := c.GetHeader("Authorization")
|
||||
if tokenString == "" {
|
||||
ResponseJSON(c, http.StatusUnauthorized, "Authorization token required", nil)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// parse and validate the token
|
||||
_, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
// Validate the signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtSecret, nil
|
||||
})
|
||||
if err != nil {
|
||||
ResponseJSON(c, http.StatusUnauthorized, "Invalid token", nil)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// Token is valid, proceed to the next handler
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -648,33 +648,33 @@ Next, update the `handlers.go` file with a `GenerateJWT` function that generates
|
||||
```go
|
||||
package api
|
||||
|
||||
import (
|
||||
//other imports
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
import (
|
||||
//other imports
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func GenerateJWT(c *gin.Context) {
|
||||
var loginRequest LoginRequest
|
||||
if err := c.ShouldBindJSON(&loginRequest); err != nil {
|
||||
ResponseJSON(c, http.StatusBadRequest, "Invalid request payload", nil)
|
||||
return
|
||||
}
|
||||
if loginRequest.Username != "admin" || loginRequest.Password != "password" {
|
||||
ResponseJSON(c, http.StatusUnauthorized, "Invalid credentials", nil)
|
||||
return
|
||||
}
|
||||
expirationTime := time.Now().Add(15 * time.Minute)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"exp": expirationTime.Unix(),
|
||||
})
|
||||
// Sign the token
|
||||
tokenString, err := token.SignedString(jwtSecret)
|
||||
if err != nil {
|
||||
ResponseJSON(c, http.StatusInternalServerError, "Could not generate token", nil)
|
||||
return
|
||||
}
|
||||
ResponseJSON(c, http.StatusOK, "Token generated successfully", gin.H{"token": tokenString})
|
||||
}
|
||||
func GenerateJWT(c *gin.Context) {
|
||||
var loginRequest LoginRequest
|
||||
if err := c.ShouldBindJSON(&loginRequest); err != nil {
|
||||
ResponseJSON(c, http.StatusBadRequest, "Invalid request payload", nil)
|
||||
return
|
||||
}
|
||||
if loginRequest.Username != "admin" || loginRequest.Password != "password" {
|
||||
ResponseJSON(c, http.StatusUnauthorized, "Invalid credentials", nil)
|
||||
return
|
||||
}
|
||||
expirationTime := time.Now().Add(15 * time.Minute)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"exp": expirationTime.Unix(),
|
||||
})
|
||||
// Sign the token
|
||||
tokenString, err := token.SignedString(jwtSecret)
|
||||
if err != nil {
|
||||
ResponseJSON(c, http.StatusInternalServerError, "Could not generate token", nil)
|
||||
return
|
||||
}
|
||||
ResponseJSON(c, http.StatusOK, "Token generated successfully", gin.H{"token": tokenString})
|
||||
}
|
||||
```
|
||||
|
||||
> In a real-world application, the credentials used to generate the token will be specific to users and not the default ones.
|
||||
@@ -685,28 +685,28 @@ Lastly, update the `main.go` file inside the `cmd` folder to add a public route
|
||||
package main
|
||||
|
||||
import (
|
||||
"go_book_api/api"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go_book_api/api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
api.InitDB()
|
||||
r := gin.Default()
|
||||
api.InitDB()
|
||||
r := gin.Default()
|
||||
|
||||
// Public routes
|
||||
r.POST("/token", api.GenerateJWT)
|
||||
// Public routes
|
||||
r.POST("/token", api.GenerateJWT)
|
||||
|
||||
// protected routes
|
||||
protected := r.Group("/", api.JWTAuthMiddleware())
|
||||
{
|
||||
protected.POST("/book", api.CreateBook)
|
||||
protected.GET("/books", api.GetBooks)
|
||||
protected.GET("/book/:id", api.GetBook)
|
||||
protected.PUT("/book/:id", api.UpdateBook)
|
||||
protected.DELETE("/book/:id", api.DeleteBook)
|
||||
}
|
||||
// protected routes
|
||||
protected := r.Group("/", api.JWTAuthMiddleware())
|
||||
{
|
||||
protected.POST("/book", api.CreateBook)
|
||||
protected.GET("/books", api.GetBooks)
|
||||
protected.GET("/book/:id", api.GetBook)
|
||||
protected.PUT("/book/:id", api.UpdateBook)
|
||||
protected.DELETE("/book/:id", api.DeleteBook)
|
||||
}
|
||||
|
||||
r.Run(":8080")
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -750,54 +750,54 @@ Since the API route is now protected, you also need to update the unit test to c
|
||||
package tests
|
||||
|
||||
import (
|
||||
// other imports
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
// other imports
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var jwtSecret = []byte(os.Getenv("SECRET_TOKEN"))
|
||||
|
||||
func setupTestDB() {
|
||||
// setupTestDB goes here
|
||||
// setupTestDB goes here
|
||||
}
|
||||
|
||||
func addBook() api.Book {
|
||||
// add book code goes here
|
||||
// add book code goes here
|
||||
}
|
||||
|
||||
func generateValidToken() string {
|
||||
expirationTime := time.Now().Add(15 * time.Minute)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"exp": expirationTime.Unix(),
|
||||
})
|
||||
tokenString, _ := token.SignedString(jwtSecret)
|
||||
return tokenString
|
||||
expirationTime := time.Now().Add(15 * time.Minute)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"exp": expirationTime.Unix(),
|
||||
})
|
||||
tokenString, _ := token.SignedString(jwtSecret)
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func TestGenerateJWT(t *testing.T) {
|
||||
router := gin.Default()
|
||||
router.POST("/token", api.GenerateJWT)
|
||||
router := gin.Default()
|
||||
router.POST("/token", api.GenerateJWT)
|
||||
|
||||
loginRequest := map[string]string{
|
||||
"username": "admin",
|
||||
"password": "password",
|
||||
}
|
||||
loginRequest := map[string]string{
|
||||
"username": "admin",
|
||||
"password": "password",
|
||||
}
|
||||
|
||||
jsonValue, _ := json.Marshal(loginRequest)
|
||||
req, _ := http.NewRequest("POST", "/token", bytes.NewBuffer(jsonValue))
|
||||
jsonValue, _ := json.Marshal(loginRequest)
|
||||
req, _ := http.NewRequest("POST", "/token", bytes.NewBuffer(jsonValue))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
if status := w.Code; status != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, status)
|
||||
}
|
||||
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
|
||||
if response.Data == nil || response.Data.(map[string]interface{})["token"] == "" {
|
||||
t.Errorf("Expected token in response, got nil or empty")
|
||||
}
|
||||
if response.Data == nil || response.Data.(map[string]interface{})["token"] == "" {
|
||||
t.Errorf("Expected token in response, got nil or empty")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -807,34 +807,34 @@ Next, use the `generateValidToken` helper function to modify the request object
|
||||
|
||||
```go
|
||||
func TestCreateBook(t *testing.T) {
|
||||
setupTestDB()
|
||||
router := gin.Default()
|
||||
protected := router.Group("/", api.JWTAuthMiddleware()) // add
|
||||
protected.POST("/book", api.CreateBook) // add
|
||||
setupTestDB()
|
||||
router := gin.Default()
|
||||
protected := router.Group("/", api.JWTAuthMiddleware()) // add
|
||||
protected.POST("/book", api.CreateBook) // add
|
||||
|
||||
token := generateValidToken() // add
|
||||
token := generateValidToken() // add
|
||||
|
||||
book := api.Book{
|
||||
Title: "Demo Book name", Author: "Demo Author name", Year: 2021,
|
||||
}
|
||||
jsonValue, _ := json.Marshal(book)
|
||||
book := api.Book{
|
||||
Title: "Demo Book name", Author: "Demo Author name", Year: 2021,
|
||||
}
|
||||
jsonValue, _ := json.Marshal(book)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue))
|
||||
req.Header.Set("Authorization", token) // add
|
||||
req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue))
|
||||
req.Header.Set("Authorization", token) // add
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if status := w.Code; status != http.StatusCreated {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusCreated, status)
|
||||
}
|
||||
if status := w.Code; status != http.StatusCreated {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusCreated, status)
|
||||
}
|
||||
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
var response api.JsonResponse
|
||||
json.NewDecoder(w.Body).Decode(&response)
|
||||
|
||||
if response.Data == nil {
|
||||
t.Errorf("Expected book data, got nil")
|
||||
}
|
||||
if response.Data == nil {
|
||||
t.Errorf("Expected book data, got nil")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -872,54 +872,55 @@ It includes functionality for creating, reading, updating, and deleting books, a
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/joho/godotenv"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/joho/godotenv"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
// InitDB initializes the database connection using environment variables.
|
||||
// It loads the database configuration from a .env file and migrates the Book schema.
|
||||
func InitDB() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
dsn := os.Getenv("DB_URL")
|
||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
// Migrate the schema
|
||||
if err := DB.AutoMigrate(&Book{}); err != nil {
|
||||
log.Fatal("Failed to migrate schema:", err)
|
||||
}
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
dsn := os.Getenv("DB_URL")
|
||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
// Migrate the schema
|
||||
if err := DB.AutoMigrate(&Book{}); err != nil {
|
||||
log.Fatal("Failed to migrate schema:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateBook handles the creation of a new book in the database.
|
||||
// It expects a JSON payload with book details and responds with the created book.
|
||||
func CreateBook(c *gin.Context) {
|
||||
var book Book
|
||||
if err := c.ShouldBindJSON(&book); err != nil {
|
||||
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
|
||||
return
|
||||
}
|
||||
DB.Create(&book)
|
||||
ResponseJSON(c, http.StatusCreated, "Book created successfully", book)
|
||||
var book Book
|
||||
if err := c.ShouldBindJSON(&book); err != nil {
|
||||
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
|
||||
return
|
||||
}
|
||||
DB.Create(&book)
|
||||
ResponseJSON(c, http.StatusCreated, "Book created successfully", book)
|
||||
}
|
||||
|
||||
// GetBooks retrieves all books from the database.
|
||||
// It responds with a list of books.
|
||||
func GetBooks(c *gin.Context) {
|
||||
var books []Book
|
||||
DB.Find(&books)
|
||||
ResponseJSON(c, http.StatusOK, "Books retrieved successfully", books)
|
||||
var books []Book
|
||||
DB.Find(&books)
|
||||
ResponseJSON(c, http.StatusOK, "Books retrieved successfully", books)
|
||||
}
|
||||
|
||||
// other handlers goes below
|
||||
|
||||
+213
-213
@@ -80,29 +80,29 @@ It would be really fun to write a bencode parser, but parsing isn't our focus to
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/jackpal/bencode-go"
|
||||
"github.com/jackpal/bencode-go"
|
||||
)
|
||||
|
||||
type bencodeInfo struct {
|
||||
Pieces string `bencode:"pieces"`
|
||||
PieceLength int `bencode:"piece length"`
|
||||
Length int `bencode:"length"`
|
||||
Name string `bencode:"name"`
|
||||
Pieces string `bencode:"pieces"`
|
||||
PieceLength int `bencode:"piece length"`
|
||||
Length int `bencode:"length"`
|
||||
Name string `bencode:"name"`
|
||||
}
|
||||
|
||||
type bencodeTorrent struct {
|
||||
Announce string `bencode:"announce"`
|
||||
Info bencodeInfo `bencode:"info"`
|
||||
Announce string `bencode:"announce"`
|
||||
Info bencodeInfo `bencode:"info"`
|
||||
}
|
||||
|
||||
// Open parses a torrent file
|
||||
func Open(r io.Reader) (*bencodeTorrent, error) {
|
||||
bto := bencodeTorrent{}
|
||||
err := bencode.Unmarshal(r, &bto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &bto, nil
|
||||
bto := bencodeTorrent{}
|
||||
err := bencode.Unmarshal(r, &bto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &bto, nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -114,16 +114,16 @@ Notably, I split `pieces` (previously a string) into a slice of hashes (each `[2
|
||||
|
||||
```go
|
||||
type TorrentFile struct {
|
||||
Announce string
|
||||
InfoHash [20]byte
|
||||
PieceHashes [][20]byte
|
||||
PieceLength int
|
||||
Length int
|
||||
Name string
|
||||
Announce string
|
||||
InfoHash [20]byte
|
||||
PieceHashes [][20]byte
|
||||
PieceLength int
|
||||
Length int
|
||||
Name string
|
||||
}
|
||||
|
||||
func (bto *bencodeTorrent) toTorrentFile() (*TorrentFile, error) {
|
||||
// ...
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -133,21 +133,21 @@ Now that we have information about the file and its tracker, let's talk to the t
|
||||
|
||||
```go
|
||||
func (t *TorrentFile) buildTrackerURL(peerID [20]byte, port uint16) (string, error) {
|
||||
base, err := url.Parse(t.Announce)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
params := url.Values{
|
||||
"info_hash": []string{string(t.InfoHash[:])},
|
||||
"peer_id": []string{string(peerID[:])},
|
||||
"port": []string{strconv.Itoa(int(Port))},
|
||||
"uploaded": []string{"0"},
|
||||
"downloaded": []string{"0"},
|
||||
"compact": []string{"1"},
|
||||
"left": []string{strconv.Itoa(t.Length)},
|
||||
}
|
||||
base.RawQuery = params.Encode()
|
||||
return base.String(), nil
|
||||
base, err := url.Parse(t.Announce)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
params := url.Values{
|
||||
"info_hash": []string{string(t.InfoHash[:])},
|
||||
"peer_id": []string{string(peerID[:])},
|
||||
"port": []string{strconv.Itoa(int(Port))},
|
||||
"uploaded": []string{"0"},
|
||||
"downloaded": []string{"0"},
|
||||
"compact": []string{"1"},
|
||||
"left": []string{strconv.Itoa(t.Length)},
|
||||
}
|
||||
base.RawQuery = params.Encode()
|
||||
return base.String(), nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -180,25 +180,25 @@ e
|
||||
```go
|
||||
// Peer encodes connection information for a peer
|
||||
type Peer struct {
|
||||
IP net.IP
|
||||
Port uint16
|
||||
IP net.IP
|
||||
Port uint16
|
||||
}
|
||||
|
||||
// Unmarshal parses peer IP addresses and ports from a buffer
|
||||
func Unmarshal(peersBin []byte) ([]Peer, error) {
|
||||
const peerSize = 6 // 4 for IP, 2 for port
|
||||
numPeers := len(peersBin) / peerSize
|
||||
if len(peersBin)%peerSize != 0 {
|
||||
err := fmt.Errorf("Received malformed peers")
|
||||
return nil, err
|
||||
}
|
||||
peers := make([]Peer, numPeers)
|
||||
for i := 0; i < numPeers; i++ {
|
||||
offset := i * peerSize
|
||||
peers[i].IP = net.IP(peersBin[offset : offset+4])
|
||||
peers[i].Port = binary.BigEndian.Uint16(peersBin[offset+4 : offset+6])
|
||||
}
|
||||
return peers, nil
|
||||
const peerSize = 6 // 4 for IP, 2 for port
|
||||
numPeers := len(peersBin) / peerSize
|
||||
if len(peersBin)%peerSize != 0 {
|
||||
err := fmt.Errorf("Received malformed peers")
|
||||
return nil, err
|
||||
}
|
||||
peers := make([]Peer, numPeers)
|
||||
for i := 0; i < numPeers; i++ {
|
||||
offset := i * peerSize
|
||||
peers[i].IP = net.IP(peersBin[offset : offset+4])
|
||||
peers[i].Port = binary.BigEndian.Uint16(peersBin[offset+4 : offset+6])
|
||||
}
|
||||
return peers, nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -215,7 +215,7 @@ Now that we have a list of peers, it's time to connect with them and start downl
|
||||
```go
|
||||
conn, err := net.DialTimeout("tcp", peer.String(), 3*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, err
|
||||
}
|
||||
```
|
||||
|
||||
@@ -252,27 +252,27 @@ In our code, let's make a struct to represent a handshake, and write a few metho
|
||||
```go
|
||||
// A Handshake is a special message that a peer uses to identify itself
|
||||
type Handshake struct {
|
||||
Pstr string
|
||||
InfoHash [20]byte
|
||||
PeerID [20]byte
|
||||
Pstr string
|
||||
InfoHash [20]byte
|
||||
PeerID [20]byte
|
||||
}
|
||||
|
||||
// Serialize serializes the handshake to a buffer
|
||||
func (h *Handshake) Serialize() []byte {
|
||||
buf := make([]byte, len(h.Pstr)+49)
|
||||
buf[0] = byte(len(h.Pstr))
|
||||
curr := 1
|
||||
curr += copy(buf[curr:], h.Pstr)
|
||||
curr += copy(buf[curr:], make([]byte, 8)) // 8 reserved bytes
|
||||
curr += copy(buf[curr:], h.InfoHash[:])
|
||||
curr += copy(buf[curr:], h.PeerID[:])
|
||||
return buf
|
||||
buf := make([]byte, len(h.Pstr)+49)
|
||||
buf[0] = byte(len(h.Pstr))
|
||||
curr := 1
|
||||
curr += copy(buf[curr:], h.Pstr)
|
||||
curr += copy(buf[curr:], make([]byte, 8)) // 8 reserved bytes
|
||||
curr += copy(buf[curr:], h.InfoHash[:])
|
||||
curr += copy(buf[curr:], h.PeerID[:])
|
||||
return buf
|
||||
}
|
||||
|
||||
// Read parses a handshake from a stream
|
||||
func Read(r io.Reader) (*Handshake, error) {
|
||||
// Do Serialize(), but backwards
|
||||
// ...
|
||||
// Do Serialize(), but backwards
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -296,36 +296,36 @@ A message starts with a length indicator which tells us how many bytes long the
|
||||
type messageID uint8
|
||||
|
||||
const (
|
||||
MsgChoke messageID = 0
|
||||
MsgUnchoke messageID = 1
|
||||
MsgInterested messageID = 2
|
||||
MsgNotInterested messageID = 3
|
||||
MsgHave messageID = 4
|
||||
MsgBitfield messageID = 5
|
||||
MsgRequest messageID = 6
|
||||
MsgPiece messageID = 7
|
||||
MsgCancel messageID = 8
|
||||
MsgChoke messageID = 0
|
||||
MsgUnchoke messageID = 1
|
||||
MsgInterested messageID = 2
|
||||
MsgNotInterested messageID = 3
|
||||
MsgHave messageID = 4
|
||||
MsgBitfield messageID = 5
|
||||
MsgRequest messageID = 6
|
||||
MsgPiece messageID = 7
|
||||
MsgCancel messageID = 8
|
||||
)
|
||||
|
||||
// Message stores ID and payload of a message
|
||||
type Message struct {
|
||||
ID messageID
|
||||
Payload []byte
|
||||
ID messageID
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// Serialize serializes a message into a buffer of the form
|
||||
// <length prefix><message ID><payload>
|
||||
// Interprets `nil` as a keep-alive message
|
||||
func (m *Message) Serialize() []byte {
|
||||
if m == nil {
|
||||
return make([]byte, 4)
|
||||
}
|
||||
length := uint32(len(m.Payload) + 1) // +1 for id
|
||||
buf := make([]byte, 4+length)
|
||||
binary.BigEndian.PutUint32(buf[0:4], length)
|
||||
buf[4] = byte(m.ID)
|
||||
copy(buf[5:], m.Payload)
|
||||
return buf
|
||||
if m == nil {
|
||||
return make([]byte, 4)
|
||||
}
|
||||
length := uint32(len(m.Payload) + 1) // +1 for id
|
||||
buf := make([]byte, 4+length)
|
||||
binary.BigEndian.PutUint32(buf[0:4], length)
|
||||
buf[4] = byte(m.ID)
|
||||
copy(buf[5:], m.Payload)
|
||||
return buf
|
||||
}
|
||||
```
|
||||
|
||||
@@ -334,30 +334,30 @@ To read a message from a stream, we just follow the format of a message. We read
|
||||
```go
|
||||
// Read parses a message from a stream. Returns `nil` on keep-alive message
|
||||
func Read(r io.Reader) (*Message, error) {
|
||||
lengthBuf := make([]byte, 4)
|
||||
_, err := io.ReadFull(r, lengthBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
length := binary.BigEndian.Uint32(lengthBuf)
|
||||
lengthBuf := make([]byte, 4)
|
||||
_, err := io.ReadFull(r, lengthBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
length := binary.BigEndian.Uint32(lengthBuf)
|
||||
|
||||
// keep-alive message
|
||||
if length == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// keep-alive message
|
||||
if length == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
messageBuf := make([]byte, length)
|
||||
_, err = io.ReadFull(r, messageBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messageBuf := make([]byte, length)
|
||||
_, err = io.ReadFull(r, messageBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := Message{
|
||||
ID: messageID(messageBuf[0]),
|
||||
Payload: messageBuf[1:],
|
||||
}
|
||||
m := Message{
|
||||
ID: messageID(messageBuf[0]),
|
||||
Payload: messageBuf[1:],
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
return &m, nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -375,16 +375,16 @@ type Bitfield []byte
|
||||
|
||||
// HasPiece tells if a bitfield has a particular index set
|
||||
func (bf Bitfield) HasPiece(index int) bool {
|
||||
byteIndex := index / 8
|
||||
offset := index % 8
|
||||
return bf[byteIndex]>>(7-offset)&1 != 0
|
||||
byteIndex := index / 8
|
||||
offset := index % 8
|
||||
return bf[byteIndex]>>(7-offset)&1 != 0
|
||||
}
|
||||
|
||||
// SetPiece sets a bit in the bitfield
|
||||
func (bf Bitfield) SetPiece(index int) {
|
||||
byteIndex := index / 8
|
||||
offset := index % 8
|
||||
bf[byteIndex] |= 1 << (7 - offset)
|
||||
byteIndex := index / 8
|
||||
offset := index % 8
|
||||
bf[byteIndex] |= 1 << (7 - offset)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -403,23 +403,23 @@ We'll set up two channels to synchronize our concurrent workers: one for dishing
|
||||
workQueue := make(chan *pieceWork, len(t.PieceHashes))
|
||||
results := make(chan *pieceResult)
|
||||
for index, hash := range t.PieceHashes {
|
||||
length := t.calculatePieceSize(index)
|
||||
workQueue <- &pieceWork{index, hash, length}
|
||||
length := t.calculatePieceSize(index)
|
||||
workQueue <- &pieceWork{index, hash, length}
|
||||
}
|
||||
|
||||
// Start workers
|
||||
for _, peer := range t.Peers {
|
||||
go t.startDownloadWorker(peer, workQueue, results)
|
||||
go t.startDownloadWorker(peer, workQueue, results)
|
||||
}
|
||||
|
||||
// Collect results into a buffer until full
|
||||
buf := make([]byte, t.Length)
|
||||
donePieces := 0
|
||||
for donePieces < len(t.PieceHashes) {
|
||||
res := <-results
|
||||
begin, end := t.calculateBoundsForPiece(res.index)
|
||||
copy(buf[begin:end], res.buf)
|
||||
donePieces++
|
||||
res := <-results
|
||||
begin, end := t.calculateBoundsForPiece(res.index)
|
||||
copy(buf[begin:end], res.buf)
|
||||
donePieces++
|
||||
}
|
||||
close(workQueue)
|
||||
```
|
||||
@@ -430,41 +430,41 @@ We'll spawn a worker goroutine for each peer we've received from the tracker. It
|
||||
|
||||
```go
|
||||
func (t *Torrent) startDownloadWorker(peer peers.Peer, workQueue chan *pieceWork, results chan *pieceResult) {
|
||||
c, err := client.New(peer, t.PeerID, t.InfoHash)
|
||||
if err != nil {
|
||||
log.Printf("Could not handshake with %s. Disconnecting\n", peer.IP)
|
||||
return
|
||||
}
|
||||
defer c.Conn.Close()
|
||||
log.Printf("Completed handshake with %s\n", peer.IP)
|
||||
c, err := client.New(peer, t.PeerID, t.InfoHash)
|
||||
if err != nil {
|
||||
log.Printf("Could not handshake with %s. Disconnecting\n", peer.IP)
|
||||
return
|
||||
}
|
||||
defer c.Conn.Close()
|
||||
log.Printf("Completed handshake with %s\n", peer.IP)
|
||||
|
||||
c.SendUnchoke()
|
||||
c.SendInterested()
|
||||
c.SendUnchoke()
|
||||
c.SendInterested()
|
||||
|
||||
for pw := range workQueue {
|
||||
if !c.Bitfield.HasPiece(pw.index) {
|
||||
workQueue <- pw // Put piece back on the queue
|
||||
continue
|
||||
}
|
||||
for pw := range workQueue {
|
||||
if !c.Bitfield.HasPiece(pw.index) {
|
||||
workQueue <- pw // Put piece back on the queue
|
||||
continue
|
||||
}
|
||||
|
||||
// Download the piece
|
||||
buf, err := attemptDownloadPiece(c, pw)
|
||||
if err != nil {
|
||||
log.Println("Exiting", err)
|
||||
workQueue <- pw // Put piece back on the queue
|
||||
return
|
||||
}
|
||||
// Download the piece
|
||||
buf, err := attemptDownloadPiece(c, pw)
|
||||
if err != nil {
|
||||
log.Println("Exiting", err)
|
||||
workQueue <- pw // Put piece back on the queue
|
||||
return
|
||||
}
|
||||
|
||||
err = checkIntegrity(pw, buf)
|
||||
if err != nil {
|
||||
log.Printf("Piece #%d failed integrity check\n", pw.index)
|
||||
workQueue <- pw // Put piece back on the queue
|
||||
continue
|
||||
}
|
||||
err = checkIntegrity(pw, buf)
|
||||
if err != nil {
|
||||
log.Printf("Piece #%d failed integrity check\n", pw.index)
|
||||
workQueue <- pw // Put piece back on the queue
|
||||
continue
|
||||
}
|
||||
|
||||
c.SendHave(pw.index)
|
||||
results <- &pieceResult{pw.index, buf}
|
||||
}
|
||||
c.SendHave(pw.index)
|
||||
results <- &pieceResult{pw.index, buf}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -474,30 +474,30 @@ We'll keep track of each peer in a struct, and modify that struct as we read mes
|
||||
|
||||
```go
|
||||
type pieceProgress struct {
|
||||
index int
|
||||
client *client.Client
|
||||
buf []byte
|
||||
downloaded int
|
||||
requested int
|
||||
backlog int
|
||||
index int
|
||||
client *client.Client
|
||||
buf []byte
|
||||
downloaded int
|
||||
requested int
|
||||
backlog int
|
||||
}
|
||||
|
||||
func (state *pieceProgress) readMessage() error {
|
||||
msg, err := state.client.Read() // this call blocks
|
||||
switch msg.ID {
|
||||
case message.MsgUnchoke:
|
||||
state.client.Choked = false
|
||||
case message.MsgChoke:
|
||||
state.client.Choked = true
|
||||
case message.MsgHave:
|
||||
index, err := message.ParseHave(msg)
|
||||
state.client.Bitfield.SetPiece(index)
|
||||
case message.MsgPiece:
|
||||
n, err := message.ParsePiece(state.index, state.buf, msg)
|
||||
state.downloaded += n
|
||||
state.backlog--
|
||||
}
|
||||
return nil
|
||||
msg, err := state.client.Read() // this call blocks
|
||||
switch msg.ID {
|
||||
case message.MsgUnchoke:
|
||||
state.client.Choked = false
|
||||
case message.MsgChoke:
|
||||
state.client.Choked = true
|
||||
case message.MsgHave:
|
||||
index, err := message.ParseHave(msg)
|
||||
state.client.Bitfield.SetPiece(index)
|
||||
case message.MsgPiece:
|
||||
n, err := message.ParsePiece(state.index, state.buf, msg)
|
||||
state.downloaded += n
|
||||
state.backlog--
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -523,43 +523,43 @@ const MaxBlockSize = 16384
|
||||
const MaxBacklog = 5
|
||||
|
||||
func attemptDownloadPiece(c *client.Client, pw *pieceWork) ([]byte, error) {
|
||||
state := pieceProgress{
|
||||
index: pw.index,
|
||||
client: c,
|
||||
buf: make([]byte, pw.length),
|
||||
}
|
||||
state := pieceProgress{
|
||||
index: pw.index,
|
||||
client: c,
|
||||
buf: make([]byte, pw.length),
|
||||
}
|
||||
|
||||
// Setting a deadline helps get unresponsive peers unstuck.
|
||||
// 30 seconds is more than enough time to download a 262 KB piece
|
||||
c.Conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
defer c.Conn.SetDeadline(time.Time{}) // Disable the deadline
|
||||
// Setting a deadline helps get unresponsive peers unstuck.
|
||||
// 30 seconds is more than enough time to download a 262 KB piece
|
||||
c.Conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
defer c.Conn.SetDeadline(time.Time{}) // Disable the deadline
|
||||
|
||||
for state.downloaded < pw.length {
|
||||
// If unchoked, send requests until we have enough unfulfilled requests
|
||||
if !state.client.Choked {
|
||||
for state.backlog < MaxBacklog && state.requested < pw.length {
|
||||
blockSize := MaxBlockSize
|
||||
// Last block might be shorter than the typical block
|
||||
if pw.length-state.requested < blockSize {
|
||||
blockSize = pw.length - state.requested
|
||||
}
|
||||
for state.downloaded < pw.length {
|
||||
// If unchoked, send requests until we have enough unfulfilled requests
|
||||
if !state.client.Choked {
|
||||
for state.backlog < MaxBacklog && state.requested < pw.length {
|
||||
blockSize := MaxBlockSize
|
||||
// Last block might be shorter than the typical block
|
||||
if pw.length-state.requested < blockSize {
|
||||
blockSize = pw.length - state.requested
|
||||
}
|
||||
|
||||
err := c.SendRequest(pw.index, state.requested, blockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.backlog++
|
||||
state.requested += blockSize
|
||||
}
|
||||
}
|
||||
err := c.SendRequest(pw.index, state.requested, blockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.backlog++
|
||||
state.requested += blockSize
|
||||
}
|
||||
}
|
||||
|
||||
err := state.readMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err := state.readMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return state.buf, nil
|
||||
return state.buf, nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -571,25 +571,25 @@ This is a short one. We're almost there.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/veggiedefender/torrent-client/torrentfile"
|
||||
"github.com/veggiedefender/torrent-client/torrentfile"
|
||||
)
|
||||
|
||||
func main() {
|
||||
inPath := os.Args[1]
|
||||
outPath := os.Args[2]
|
||||
inPath := os.Args[1]
|
||||
outPath := os.Args[2]
|
||||
|
||||
tf, err := torrentfile.Open(inPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tf, err := torrentfile.Open(inPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = tf.DownloadToFile(outPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = tf.DownloadToFile(outPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in new issue
Block a user