From 32690e98dafaac921f90b593e9ce27998bef4ac0 Mon Sep 17 00:00:00 2001 From: Piotr Idzik <65706193+vil02@users.noreply.github.com> Date: Thu, 20 Feb 2025 01:42:14 +0100 Subject: [PATCH] Use `gofmt` compliant formatting (#8192) --- src/data/guides/go-vs-java.md | 10 +- src/data/guides/golang-rest-api.md | 715 +++++++++++++++-------------- src/data/guides/torrent-client.md | 458 +++++++++--------- 3 files changed, 592 insertions(+), 591 deletions(-) diff --git a/src/data/guides/go-vs-java.md b/src/data/guides/go-vs-java.md index df483810c..022868b36 100644 --- a/src/data/guides/go-vs-java.md +++ b/src/data/guides/go-vs-java.md @@ -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) } ``` diff --git a/src/data/guides/golang-rest-api.md b/src/data/guides/golang-rest-api.md index 5ac906ba3..f2caef2e6 100644 --- a/src/data/guides/golang-rest-api.md +++ b/src/data/guides/golang-rest-api.md @@ -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) - } - - 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) + } } func CreateBook(c *gin.Context) { - 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) + 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) } ``` @@ -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 - } - - // 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) + 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 + } + + 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) - - 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) - - 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) - - 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) - } + 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) + + 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) + } + + 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) + } } ``` @@ -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) - - 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) - } - - 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) - } - - //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") - } + 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) + + 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) + + 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") + } } ``` @@ -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" - ) - - 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}) - } +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}) +} ``` > 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() - - // 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) - } - - r.Run(":8080") + api.InitDB() + r := gin.Default() + + // 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) + } + + 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 diff --git a/src/data/guides/torrent-client.md b/src/data/guides/torrent-client.md index 62d299e28..514efb7d5 100644 --- a/src/data/guides/torrent-client.md +++ b/src/data/guides/torrent-client.md @@ -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 // // 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) - - // keep-alive message - if length == 0 { - return nil, nil - } - - messageBuf := make([]byte, length) - _, err = io.ReadFull(r, messageBuf) - if err != nil { - return nil, err - } - - m := Message{ - ID: messageID(messageBuf[0]), - Payload: messageBuf[1:], - } - - return &m, nil + 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 + } + + messageBuf := make([]byte, length) + _, err = io.ReadFull(r, messageBuf) + if err != nil { + return nil, err + } + + m := Message{ + ID: messageID(messageBuf[0]), + Payload: messageBuf[1:], + } + + 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.SendUnchoke() - c.SendInterested() - - 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 - } - - 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, 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() + + 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 + } + + 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} + } } ``` @@ -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), - } - - // 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 - } - - 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 - } - } - - return state.buf, nil + 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 + + 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 := state.readMessage() + if err != nil { + return nil, err + } + } + + 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] - - tf, err := torrentfile.Open(inPath) - if err != nil { - log.Fatal(err) - } - - err = tf.DownloadToFile(outPath) - if err != nil { - log.Fatal(err) - } + inPath := os.Args[1] + outPath := os.Args[2] + + tf, err := torrentfile.Open(inPath) + if err != nil { + log.Fatal(err) + } + + err = tf.DownloadToFile(outPath) + if err != nil { + log.Fatal(err) + } } ```