Use `gofmt` compliant formatting (#8192)

pull/8245/head
Piotr Idzik 3 days ago committed by GitHub
parent ed8c2f3168
commit 32690e98da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      src/data/guides/go-vs-java.md
  2. 715
      src/data/guides/golang-rest-api.md
  3. 458
      src/data/guides/torrent-client.md

@ -104,16 +104,16 @@ Go and Java are powerful languages with distinct approaches to handling errors.
```go ```go
func divide(a, b int) (int, error) { func divide(a, b int) (int, error) {
if b == 0 { if b == 0 {
return 0, errors.New("division by zero") return 0, errors.New("division by zero")
} }
return a / b, nil return a / b, nil
} }
// Usage // Usage
result, err := divide(10, 0) result, err := divide(10, 0)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
``` ```

@ -105,26 +105,26 @@ package api
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
type Book struct { type Book struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"` Title string `json:"title"`
Author string `json:"author"` Author string `json:"author"`
Year int `json:"year"` Year int `json:"year"`
} }
type JsonResponse struct { type JsonResponse struct {
Status int `json:"status"` Status int `json:"status"`
Message string `json:"message"` Message string `json:"message"`
Data any `json:"data"` Data any `json:"data"`
} }
func ResponseJSON(c *gin.Context, status int, message string, data any) { func ResponseJSON(c *gin.Context, status int, message string, data any) {
response := JsonResponse{ response := JsonResponse{
Status: status, Status: status,
Message: message, Message: message,
Data: data, 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 package api
import ( import (
"log" "github.com/gin-gonic/gin"
"net/http" "github.com/joho/godotenv"
"os" "gorm.io/driver/postgres"
"github.com/gin-gonic/gin" "gorm.io/gorm"
"github.com/joho/godotenv" "log"
"gorm.io/driver/postgres" "net/http"
"gorm.io/gorm" "os"
) )
var DB *gorm.DB var DB *gorm.DB
func InitDB() { func InitDB() {
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
log.Fatal("Failed to connect to database:", err) log.Fatal("Failed to connect to database:", err)
} }
dsn := os.Getenv("DB_URL") dsn := os.Getenv("DB_URL")
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil { if err != nil {
log.Fatal("Failed to connect to database:", err) log.Fatal("Failed to connect to database:", err)
} }
// migrate the schema // migrate the schema
if err := DB.AutoMigrate(&Book{}); err != nil { if err := DB.AutoMigrate(&Book{}); err != nil {
log.Fatal("Failed to migrate schema:", err) log.Fatal("Failed to migrate schema:", err)
} }
} }
func CreateBook(c *gin.Context) { func CreateBook(c *gin.Context) {
var book Book var book Book
//bind the request body //bind the request body
if err := c.ShouldBindJSON(&book); err != nil { if err := c.ShouldBindJSON(&book); err != nil {
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil) ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
return return
} }
DB.Create(&book) DB.Create(&book)
ResponseJSON(c, http.StatusCreated, "Book created successfully", 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 ```go
func GetBooks(c *gin.Context) { func GetBooks(c *gin.Context) {
var books []Book var books []Book
DB.Find(&books) DB.Find(&books)
ResponseJSON(c, http.StatusOK, "Books retrieved successfully", 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 ```go
func GetBook(c *gin.Context) { func GetBook(c *gin.Context) {
var book Book var book Book
if err := DB.First(&book, c.Param("id")).Error; err != nil { if err := DB.First(&book, c.Param("id")).Error; err != nil {
ResponseJSON(c, http.StatusNotFound, "Book not found", nil) ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
return return
} }
ResponseJSON(c, http.StatusOK, "Book retrieved successfully", book) 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 ```go
func UpdateBook(c *gin.Context) { func UpdateBook(c *gin.Context) {
var book Book var book Book
if err := DB.First(&book, c.Param("id")).Error; err != nil { if err := DB.First(&book, c.Param("id")).Error; err != nil {
ResponseJSON(c, http.StatusNotFound, "Book not found", nil) ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
return return
} }
// bind the request body // bind the request body
if err := c.ShouldBindJSON(&book); err != nil { if err := c.ShouldBindJSON(&book); err != nil {
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil) ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
return return
} }
DB.Save(&book) DB.Save(&book)
ResponseJSON(c, http.StatusOK, "Book updated successfully", 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 ```go
func DeleteBook(c *gin.Context) { func DeleteBook(c *gin.Context) {
var book Book var book Book
if err := DB.Delete(&book, c.Param("id")).Error; err != nil { if err := DB.Delete(&book, c.Param("id")).Error; err != nil {
ResponseJSON(c, http.StatusNotFound, "Book not found", nil) ResponseJSON(c, http.StatusNotFound, "Book not found", nil)
return return
} }
ResponseJSON(c, http.StatusOK, "Book deleted successfully", nil) ResponseJSON(c, http.StatusOK, "Book deleted successfully", nil)
} }
``` ```
### Putting it all together ### 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 package main
import ( import (
"go_book_api/api" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin" "go_book_api/api"
) )
func main() { func main() {
api.InitDB() api.InitDB()
r := gin.Default() r := gin.Default()
//routes //routes
r.POST("/book", api.CreateBook) r.POST("/book", api.CreateBook)
r.GET("/books", api.GetBooks) r.GET("/books", api.GetBooks)
r.GET("/book/:id", api.GetBook) r.GET("/book/:id", api.GetBook)
r.PUT("/book/:id", api.UpdateBook) r.PUT("/book/:id", api.UpdateBook)
r.DELETE("/book/:id", api.DeleteBook) 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 package tests
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"go_book_api/api" "github.com/gin-gonic/gin"
"net/http" "go_book_api/api"
"net/http/httptest" "gorm.io/driver/sqlite"
"strconv" "gorm.io/gorm"
"testing" "net/http"
"github.com/gin-gonic/gin" "net/http/httptest"
"gorm.io/driver/sqlite" "strconv"
"gorm.io/gorm" "testing"
) )
func setupTestDB() { func setupTestDB() {
var err error var err error
api.DB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) api.DB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil { if err != nil {
panic("failed to connect test database") panic("failed to connect test database")
} }
api.DB.AutoMigrate(&api.Book{}) api.DB.AutoMigrate(&api.Book{})
} }
func addBook() api.Book { func addBook() api.Book {
book := api.Book{Title: "Go Programming", Author: "John Doe", Year: 2023} book := api.Book{Title: "Go Programming", Author: "John Doe", Year: 2023}
api.DB.Create(&book) api.DB.Create(&book)
return book return book
} }
``` ```
@ -404,29 +404,29 @@ Create a `TestCreateBook` test function that sets up a mock HTTP server using Gi
```go ```go
func TestCreateBook(t *testing.T) { func TestCreateBook(t *testing.T) {
setupTestDB() setupTestDB()
router := gin.Default() router := gin.Default()
router.POST("/book", api.CreateBook) router.POST("/book", api.CreateBook)
book := api.Book{ book := api.Book{
Title: "Demo Book name", Author: "Demo Author name", Year: 2021, Title: "Demo Book name", Author: "Demo Author name", Year: 2021,
} }
jsonValue, _ := json.Marshal(book) jsonValue, _ := json.Marshal(book)
req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue)) req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue))
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusCreated { if status := w.Code; status != http.StatusCreated {
t.Errorf("Expected status %d, got %d", http.StatusCreated, status) t.Errorf("Expected status %d, got %d", http.StatusCreated, status)
} }
var response api.JsonResponse var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response) json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil { if response.Data == nil {
t.Errorf("Expected book data, got nil") t.Errorf("Expected book data, got nil")
} }
} }
``` ```
@ -436,25 +436,25 @@ Similar to the `TestCreateBook` test function, create a `TestGetBooks` that test
```go ```go
func TestGetBooks(t *testing.T) { func TestGetBooks(t *testing.T) {
setupTestDB() setupTestDB()
addBook() addBook()
router := gin.Default() router := gin.Default()
router.GET("/books", api.GetBooks) router.GET("/books", api.GetBooks)
req, _ := http.NewRequest("GET", "/books", nil) req, _ := http.NewRequest("GET", "/books", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK { if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status) t.Errorf("Expected status %d, got %d", http.StatusOK, status)
} }
var response api.JsonResponse var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response) json.NewDecoder(w.Body).Decode(&response)
if len(response.Data.([]interface{})) == 0 { if len(response.Data.([]interface{})) == 0 {
t.Errorf("Expected non-empty books list") t.Errorf("Expected non-empty books list")
} }
} }
``` ```
@ -464,25 +464,25 @@ Create a `TestGetBook` test function that tests the `GetBook` handler functional
```go ```go
func TestGetBook(t *testing.T) { func TestGetBook(t *testing.T) {
setupTestDB() setupTestDB()
book := addBook() book := addBook()
router := gin.Default() router := gin.Default()
router.GET("/book/:id", api.GetBook) router.GET("/book/:id", api.GetBook)
req, _ := http.NewRequest("GET", "/book/"+strconv.Itoa(int(book.ID)), nil) req, _ := http.NewRequest("GET", "/book/"+strconv.Itoa(int(book.ID)), nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK { if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status) t.Errorf("Expected status %d, got %d", http.StatusOK, status)
} }
var response api.JsonResponse var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response) json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil || response.Data.(map[string]interface{})["id"] != float64(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) 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 ```go
func TestUpdateBook(t *testing.T) { func TestUpdateBook(t *testing.T) {
setupTestDB() setupTestDB()
book := addBook() book := addBook()
router := gin.Default() router := gin.Default()
router.PUT("/book/:id", api.UpdateBook) router.PUT("/book/:id", api.UpdateBook)
updateBook := api.Book{ updateBook := api.Book{
Title: "Advanced Go Programming", Author: "Demo Author name", Year: 2021, Title: "Advanced Go Programming", Author: "Demo Author name", Year: 2021,
} }
jsonValue, _ := json.Marshal(updateBook) jsonValue, _ := json.Marshal(updateBook)
req, _ := http.NewRequest("PUT", "/book/"+strconv.Itoa(int(book.ID)), bytes.NewBuffer(jsonValue)) req, _ := http.NewRequest("PUT", "/book/"+strconv.Itoa(int(book.ID)), bytes.NewBuffer(jsonValue))
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK { if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status) t.Errorf("Expected status %d, got %d", http.StatusOK, status)
} }
var response api.JsonResponse var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response) json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil || response.Data.(map[string]interface{})["title"] != "Advanced Go Programming" { 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) 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 ```go
func TestDeleteBook(t *testing.T) { func TestDeleteBook(t *testing.T) {
setupTestDB() setupTestDB()
book := addBook() book := addBook()
router := gin.Default() router := gin.Default()
router.DELETE("/book/:id", api.DeleteBook) router.DELETE("/book/:id", api.DeleteBook)
req, _ := http.NewRequest("DELETE", "/book/"+strconv.Itoa(int(book.ID)), nil) req, _ := http.NewRequest("DELETE", "/book/"+strconv.Itoa(int(book.ID)), nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK { if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status) t.Errorf("Expected status %d, got %d", http.StatusOK, status)
} }
var response api.JsonResponse var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response) json.NewDecoder(w.Body).Decode(&response)
if response.Message != "Book deleted successfully" { if response.Message != "Book deleted successfully" {
t.Errorf("Expected delete message 'Book deleted successfully', got %v", response.Message) t.Errorf("Expected delete message 'Book deleted successfully', got %v", response.Message)
} }
//verify that the book was deleted //verify that the book was deleted
var deletedBook api.Book var deletedBook api.Book
result := api.DB.First(&deletedBook, book.ID) result := api.DB.First(&deletedBook, book.ID)
if result.Error == nil { if result.Error == nil {
t.Errorf("Expected book to be deleted, but it still exists") 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 package api
import ( import (
"fmt" "fmt"
"net/http" "github.com/gin-gonic/gin"
"os" "github.com/golang-jwt/jwt/v5"
"github.com/gin-gonic/gin" "net/http"
"github.com/golang-jwt/jwt/v5" "os"
) )
// Secret key for signing JWT // Secret key for signing JWT
var jwtSecret = []byte(os.Getenv("SECRET_TOKEN")) var jwtSecret = []byte(os.Getenv("SECRET_TOKEN"))
func JWTAuthMiddleware() gin.HandlerFunc { func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization") tokenString := c.GetHeader("Authorization")
if tokenString == "" { if tokenString == "" {
ResponseJSON(c, http.StatusUnauthorized, "Authorization token required", nil) ResponseJSON(c, http.StatusUnauthorized, "Authorization token required", nil)
c.Abort() c.Abort()
return return
} }
// parse and validate the token // parse and validate the token
_, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { _, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method // Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
} }
return jwtSecret, nil return jwtSecret, nil
}) })
if err != nil { if err != nil {
ResponseJSON(c, http.StatusUnauthorized, "Invalid token", nil) ResponseJSON(c, http.StatusUnauthorized, "Invalid token", nil)
c.Abort() c.Abort()
return return
} }
// Token is valid, proceed to the next handler // Token is valid, proceed to the next handler
c.Next() c.Next()
} }
} }
``` ```
@ -648,33 +648,33 @@ Next, update the `handlers.go` file with a `GenerateJWT` function that generates
```go ```go
package api package api
import ( import (
//other imports //other imports
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
func GenerateJWT(c *gin.Context) { func GenerateJWT(c *gin.Context) {
var loginRequest LoginRequest var loginRequest LoginRequest
if err := c.ShouldBindJSON(&loginRequest); err != nil { if err := c.ShouldBindJSON(&loginRequest); err != nil {
ResponseJSON(c, http.StatusBadRequest, "Invalid request payload", nil) ResponseJSON(c, http.StatusBadRequest, "Invalid request payload", nil)
return return
} }
if loginRequest.Username != "admin" || loginRequest.Password != "password" { if loginRequest.Username != "admin" || loginRequest.Password != "password" {
ResponseJSON(c, http.StatusUnauthorized, "Invalid credentials", nil) ResponseJSON(c, http.StatusUnauthorized, "Invalid credentials", nil)
return return
} }
expirationTime := time.Now().Add(15 * time.Minute) expirationTime := time.Now().Add(15 * time.Minute)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"exp": expirationTime.Unix(), "exp": expirationTime.Unix(),
}) })
// Sign the token // Sign the token
tokenString, err := token.SignedString(jwtSecret) tokenString, err := token.SignedString(jwtSecret)
if err != nil { if err != nil {
ResponseJSON(c, http.StatusInternalServerError, "Could not generate token", nil) ResponseJSON(c, http.StatusInternalServerError, "Could not generate token", nil)
return return
} }
ResponseJSON(c, http.StatusOK, "Token generated successfully", gin.H{"token": tokenString}) 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. > 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 package main
import ( import (
"go_book_api/api" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin" "go_book_api/api"
) )
func main() { func main() {
api.InitDB() api.InitDB()
r := gin.Default() r := gin.Default()
// Public routes // Public routes
r.POST("/token", api.GenerateJWT) r.POST("/token", api.GenerateJWT)
// protected routes // protected routes
protected := r.Group("/", api.JWTAuthMiddleware()) protected := r.Group("/", api.JWTAuthMiddleware())
{ {
protected.POST("/book", api.CreateBook) protected.POST("/book", api.CreateBook)
protected.GET("/books", api.GetBooks) protected.GET("/books", api.GetBooks)
protected.GET("/book/:id", api.GetBook) protected.GET("/book/:id", api.GetBook)
protected.PUT("/book/:id", api.UpdateBook) protected.PUT("/book/:id", api.UpdateBook)
protected.DELETE("/book/:id", api.DeleteBook) 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 package tests
import ( import (
// other imports // other imports
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
var jwtSecret = []byte(os.Getenv("SECRET_TOKEN")) var jwtSecret = []byte(os.Getenv("SECRET_TOKEN"))
func setupTestDB() { func setupTestDB() {
// setupTestDB goes here // setupTestDB goes here
} }
func addBook() api.Book { func addBook() api.Book {
// add book code goes here // add book code goes here
} }
func generateValidToken() string { func generateValidToken() string {
expirationTime := time.Now().Add(15 * time.Minute) expirationTime := time.Now().Add(15 * time.Minute)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"exp": expirationTime.Unix(), "exp": expirationTime.Unix(),
}) })
tokenString, _ := token.SignedString(jwtSecret) tokenString, _ := token.SignedString(jwtSecret)
return tokenString return tokenString
} }
func TestGenerateJWT(t *testing.T) { func TestGenerateJWT(t *testing.T) {
router := gin.Default() router := gin.Default()
router.POST("/token", api.GenerateJWT) router.POST("/token", api.GenerateJWT)
loginRequest := map[string]string{ loginRequest := map[string]string{
"username": "admin", "username": "admin",
"password": "password", "password": "password",
} }
jsonValue, _ := json.Marshal(loginRequest) jsonValue, _ := json.Marshal(loginRequest)
req, _ := http.NewRequest("POST", "/token", bytes.NewBuffer(jsonValue)) req, _ := http.NewRequest("POST", "/token", bytes.NewBuffer(jsonValue))
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusOK { if status := w.Code; status != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, status) t.Errorf("Expected status %d, got %d", http.StatusOK, status)
} }
var response api.JsonResponse var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response) json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil || response.Data.(map[string]interface{})["token"] == "" { if response.Data == nil || response.Data.(map[string]interface{})["token"] == "" {
t.Errorf("Expected token in response, got nil or empty") 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 ```go
func TestCreateBook(t *testing.T) { func TestCreateBook(t *testing.T) {
setupTestDB() setupTestDB()
router := gin.Default() router := gin.Default()
protected := router.Group("/", api.JWTAuthMiddleware()) // add protected := router.Group("/", api.JWTAuthMiddleware()) // add
protected.POST("/book", api.CreateBook) // add protected.POST("/book", api.CreateBook) // add
token := generateValidToken() // add token := generateValidToken() // add
book := api.Book{ book := api.Book{
Title: "Demo Book name", Author: "Demo Author name", Year: 2021, Title: "Demo Book name", Author: "Demo Author name", Year: 2021,
} }
jsonValue, _ := json.Marshal(book) jsonValue, _ := json.Marshal(book)
req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue)) req, _ := http.NewRequest("POST", "/book", bytes.NewBuffer(jsonValue))
req.Header.Set("Authorization", token) // add req.Header.Set("Authorization", token) // add
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
if status := w.Code; status != http.StatusCreated { if status := w.Code; status != http.StatusCreated {
t.Errorf("Expected status %d, got %d", http.StatusCreated, status) t.Errorf("Expected status %d, got %d", http.StatusCreated, status)
} }
var response api.JsonResponse var response api.JsonResponse
json.NewDecoder(w.Body).Decode(&response) json.NewDecoder(w.Body).Decode(&response)
if response.Data == nil { if response.Data == nil {
t.Errorf("Expected book data, got 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 package api
import ( import (
"log" "github.com/gin-gonic/gin"
"net/http" "github.com/golang-jwt/jwt/v5"
"os" "github.com/joho/godotenv"
"time" "gorm.io/driver/postgres"
"github.com/gin-gonic/gin" "gorm.io/gorm"
"github.com/golang-jwt/jwt/v5" "log"
"github.com/joho/godotenv" "net/http"
"gorm.io/driver/postgres" "os"
"gorm.io/gorm" "time"
) )
var DB *gorm.DB var DB *gorm.DB
// InitDB initializes the database connection using environment variables. // InitDB initializes the database connection using environment variables.
// It loads the database configuration from a .env file and migrates the Book schema. // It loads the database configuration from a .env file and migrates the Book schema.
func InitDB() { func InitDB() {
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
log.Fatal("Failed to connect to database:", err) log.Fatal("Failed to connect to database:", err)
} }
dsn := os.Getenv("DB_URL") dsn := os.Getenv("DB_URL")
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil { if err != nil {
log.Fatal("Failed to connect to database:", err) log.Fatal("Failed to connect to database:", err)
} }
// Migrate the schema // Migrate the schema
if err := DB.AutoMigrate(&Book{}); err != nil { if err := DB.AutoMigrate(&Book{}); err != nil {
log.Fatal("Failed to migrate schema:", err) log.Fatal("Failed to migrate schema:", err)
} }
} }
// CreateBook handles the creation of a new book in the database. // 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. // It expects a JSON payload with book details and responds with the created book.
func CreateBook(c *gin.Context) { func CreateBook(c *gin.Context) {
var book Book var book Book
if err := c.ShouldBindJSON(&book); err != nil { if err := c.ShouldBindJSON(&book); err != nil {
ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil) ResponseJSON(c, http.StatusBadRequest, "Invalid input", nil)
return return
} }
DB.Create(&book) DB.Create(&book)
ResponseJSON(c, http.StatusCreated, "Book created successfully", book) ResponseJSON(c, http.StatusCreated, "Book created successfully", book)
} }
// GetBooks retrieves all books from the database. // GetBooks retrieves all books from the database.
// It responds with a list of books. // It responds with a list of books.
func GetBooks(c *gin.Context) { func GetBooks(c *gin.Context) {
var books []Book var books []Book
DB.Find(&books) DB.Find(&books)
ResponseJSON(c, http.StatusOK, "Books retrieved successfully", books) ResponseJSON(c, http.StatusOK, "Books retrieved successfully", books)
} }
// other handlers goes below // other handlers goes below

@ -80,29 +80,29 @@ It would be really fun to write a bencode parser, but parsing isn't our focus to
```go ```go
import ( import (
"github.com/jackpal/bencode-go" "github.com/jackpal/bencode-go"
) )
type bencodeInfo struct { type bencodeInfo struct {
Pieces string `bencode:"pieces"` Pieces string `bencode:"pieces"`
PieceLength int `bencode:"piece length"` PieceLength int `bencode:"piece length"`
Length int `bencode:"length"` Length int `bencode:"length"`
Name string `bencode:"name"` Name string `bencode:"name"`
} }
type bencodeTorrent struct { type bencodeTorrent struct {
Announce string `bencode:"announce"` Announce string `bencode:"announce"`
Info bencodeInfo `bencode:"info"` Info bencodeInfo `bencode:"info"`
} }
// Open parses a torrent file // Open parses a torrent file
func Open(r io.Reader) (*bencodeTorrent, error) { func Open(r io.Reader) (*bencodeTorrent, error) {
bto := bencodeTorrent{} bto := bencodeTorrent{}
err := bencode.Unmarshal(r, &bto) err := bencode.Unmarshal(r, &bto)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &bto, nil return &bto, nil
} }
``` ```
@ -114,16 +114,16 @@ Notably, I split `pieces` (previously a string) into a slice of hashes (each `[2
```go ```go
type TorrentFile struct { type TorrentFile struct {
Announce string Announce string
InfoHash [20]byte InfoHash [20]byte
PieceHashes [][20]byte PieceHashes [][20]byte
PieceLength int PieceLength int
Length int Length int
Name string Name string
} }
func (bto *bencodeTorrent) toTorrentFile() (*TorrentFile, error) { 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 ```go
func (t *TorrentFile) buildTrackerURL(peerID [20]byte, port uint16) (string, error) { func (t *TorrentFile) buildTrackerURL(peerID [20]byte, port uint16) (string, error) {
base, err := url.Parse(t.Announce) base, err := url.Parse(t.Announce)
if err != nil { if err != nil {
return "", err return "", err
} }
params := url.Values{ params := url.Values{
"info_hash": []string{string(t.InfoHash[:])}, "info_hash": []string{string(t.InfoHash[:])},
"peer_id": []string{string(peerID[:])}, "peer_id": []string{string(peerID[:])},
"port": []string{strconv.Itoa(int(Port))}, "port": []string{strconv.Itoa(int(Port))},
"uploaded": []string{"0"}, "uploaded": []string{"0"},
"downloaded": []string{"0"}, "downloaded": []string{"0"},
"compact": []string{"1"}, "compact": []string{"1"},
"left": []string{strconv.Itoa(t.Length)}, "left": []string{strconv.Itoa(t.Length)},
} }
base.RawQuery = params.Encode() base.RawQuery = params.Encode()
return base.String(), nil return base.String(), nil
} }
``` ```
@ -180,25 +180,25 @@ e
```go ```go
// Peer encodes connection information for a peer // Peer encodes connection information for a peer
type Peer struct { type Peer struct {
IP net.IP IP net.IP
Port uint16 Port uint16
} }
// Unmarshal parses peer IP addresses and ports from a buffer // Unmarshal parses peer IP addresses and ports from a buffer
func Unmarshal(peersBin []byte) ([]Peer, error) { func Unmarshal(peersBin []byte) ([]Peer, error) {
const peerSize = 6 // 4 for IP, 2 for port const peerSize = 6 // 4 for IP, 2 for port
numPeers := len(peersBin) / peerSize numPeers := len(peersBin) / peerSize
if len(peersBin)%peerSize != 0 { if len(peersBin)%peerSize != 0 {
err := fmt.Errorf("Received malformed peers") err := fmt.Errorf("Received malformed peers")
return nil, err return nil, err
} }
peers := make([]Peer, numPeers) peers := make([]Peer, numPeers)
for i := 0; i < numPeers; i++ { for i := 0; i < numPeers; i++ {
offset := i * peerSize offset := i * peerSize
peers[i].IP = net.IP(peersBin[offset : offset+4]) peers[i].IP = net.IP(peersBin[offset : offset+4])
peers[i].Port = binary.BigEndian.Uint16(peersBin[offset+4 : offset+6]) peers[i].Port = binary.BigEndian.Uint16(peersBin[offset+4 : offset+6])
} }
return peers, nil 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 ```go
conn, err := net.DialTimeout("tcp", peer.String(), 3*time.Second) conn, err := net.DialTimeout("tcp", peer.String(), 3*time.Second)
if err != nil { 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 ```go
// A Handshake is a special message that a peer uses to identify itself // A Handshake is a special message that a peer uses to identify itself
type Handshake struct { type Handshake struct {
Pstr string Pstr string
InfoHash [20]byte InfoHash [20]byte
PeerID [20]byte PeerID [20]byte
} }
// Serialize serializes the handshake to a buffer // Serialize serializes the handshake to a buffer
func (h *Handshake) Serialize() []byte { func (h *Handshake) Serialize() []byte {
buf := make([]byte, len(h.Pstr)+49) buf := make([]byte, len(h.Pstr)+49)
buf[0] = byte(len(h.Pstr)) buf[0] = byte(len(h.Pstr))
curr := 1 curr := 1
curr += copy(buf[curr:], h.Pstr) curr += copy(buf[curr:], h.Pstr)
curr += copy(buf[curr:], make([]byte, 8)) // 8 reserved bytes curr += copy(buf[curr:], make([]byte, 8)) // 8 reserved bytes
curr += copy(buf[curr:], h.InfoHash[:]) curr += copy(buf[curr:], h.InfoHash[:])
curr += copy(buf[curr:], h.PeerID[:]) curr += copy(buf[curr:], h.PeerID[:])
return buf return buf
} }
// Read parses a handshake from a stream // Read parses a handshake from a stream
func Read(r io.Reader) (*Handshake, error) { 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 type messageID uint8
const ( const (
MsgChoke messageID = 0 MsgChoke messageID = 0
MsgUnchoke messageID = 1 MsgUnchoke messageID = 1
MsgInterested messageID = 2 MsgInterested messageID = 2
MsgNotInterested messageID = 3 MsgNotInterested messageID = 3
MsgHave messageID = 4 MsgHave messageID = 4
MsgBitfield messageID = 5 MsgBitfield messageID = 5
MsgRequest messageID = 6 MsgRequest messageID = 6
MsgPiece messageID = 7 MsgPiece messageID = 7
MsgCancel messageID = 8 MsgCancel messageID = 8
) )
// Message stores ID and payload of a message // Message stores ID and payload of a message
type Message struct { type Message struct {
ID messageID ID messageID
Payload []byte Payload []byte
} }
// Serialize serializes a message into a buffer of the form // Serialize serializes a message into a buffer of the form
// <length prefix><message ID><payload> // <length prefix><message ID><payload>
// Interprets `nil` as a keep-alive message // Interprets `nil` as a keep-alive message
func (m *Message) Serialize() []byte { func (m *Message) Serialize() []byte {
if m == nil { if m == nil {
return make([]byte, 4) return make([]byte, 4)
} }
length := uint32(len(m.Payload) + 1) // +1 for id length := uint32(len(m.Payload) + 1) // +1 for id
buf := make([]byte, 4+length) buf := make([]byte, 4+length)
binary.BigEndian.PutUint32(buf[0:4], length) binary.BigEndian.PutUint32(buf[0:4], length)
buf[4] = byte(m.ID) buf[4] = byte(m.ID)
copy(buf[5:], m.Payload) copy(buf[5:], m.Payload)
return buf return buf
} }
``` ```
@ -334,30 +334,30 @@ To read a message from a stream, we just follow the format of a message. We read
```go ```go
// Read parses a message from a stream. Returns `nil` on keep-alive message // Read parses a message from a stream. Returns `nil` on keep-alive message
func Read(r io.Reader) (*Message, error) { func Read(r io.Reader) (*Message, error) {
lengthBuf := make([]byte, 4) lengthBuf := make([]byte, 4)
_, err := io.ReadFull(r, lengthBuf) _, err := io.ReadFull(r, lengthBuf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
length := binary.BigEndian.Uint32(lengthBuf) length := binary.BigEndian.Uint32(lengthBuf)
// keep-alive message // keep-alive message
if length == 0 { if length == 0 {
return nil, nil return nil, nil
} }
messageBuf := make([]byte, length) messageBuf := make([]byte, length)
_, err = io.ReadFull(r, messageBuf) _, err = io.ReadFull(r, messageBuf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m := Message{ m := Message{
ID: messageID(messageBuf[0]), ID: messageID(messageBuf[0]),
Payload: messageBuf[1:], 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 // HasPiece tells if a bitfield has a particular index set
func (bf Bitfield) HasPiece(index int) bool { func (bf Bitfield) HasPiece(index int) bool {
byteIndex := index / 8 byteIndex := index / 8
offset := index % 8 offset := index % 8
return bf[byteIndex]>>(7-offset)&1 != 0 return bf[byteIndex]>>(7-offset)&1 != 0
} }
// SetPiece sets a bit in the bitfield // SetPiece sets a bit in the bitfield
func (bf Bitfield) SetPiece(index int) { func (bf Bitfield) SetPiece(index int) {
byteIndex := index / 8 byteIndex := index / 8
offset := index % 8 offset := index % 8
bf[byteIndex] |= 1 << (7 - offset) 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)) workQueue := make(chan *pieceWork, len(t.PieceHashes))
results := make(chan *pieceResult) results := make(chan *pieceResult)
for index, hash := range t.PieceHashes { for index, hash := range t.PieceHashes {
length := t.calculatePieceSize(index) length := t.calculatePieceSize(index)
workQueue <- &pieceWork{index, hash, length} workQueue <- &pieceWork{index, hash, length}
} }
// Start workers // Start workers
for _, peer := range t.Peers { for _, peer := range t.Peers {
go t.startDownloadWorker(peer, workQueue, results) go t.startDownloadWorker(peer, workQueue, results)
} }
// Collect results into a buffer until full // Collect results into a buffer until full
buf := make([]byte, t.Length) buf := make([]byte, t.Length)
donePieces := 0 donePieces := 0
for donePieces < len(t.PieceHashes) { for donePieces < len(t.PieceHashes) {
res := <-results res := <-results
begin, end := t.calculateBoundsForPiece(res.index) begin, end := t.calculateBoundsForPiece(res.index)
copy(buf[begin:end], res.buf) copy(buf[begin:end], res.buf)
donePieces++ donePieces++
} }
close(workQueue) close(workQueue)
``` ```
@ -430,41 +430,41 @@ We'll spawn a worker goroutine for each peer we've received from the tracker. It
```go ```go
func (t *Torrent) startDownloadWorker(peer peers.Peer, workQueue chan *pieceWork, results chan *pieceResult) { func (t *Torrent) startDownloadWorker(peer peers.Peer, workQueue chan *pieceWork, results chan *pieceResult) {
c, err := client.New(peer, t.PeerID, t.InfoHash) c, err := client.New(peer, t.PeerID, t.InfoHash)
if err != nil { if err != nil {
log.Printf("Could not handshake with %s. Disconnecting\n", peer.IP) log.Printf("Could not handshake with %s. Disconnecting\n", peer.IP)
return return
} }
defer c.Conn.Close() defer c.Conn.Close()
log.Printf("Completed handshake with %s\n", peer.IP) log.Printf("Completed handshake with %s\n", peer.IP)
c.SendUnchoke() c.SendUnchoke()
c.SendInterested() c.SendInterested()
for pw := range workQueue { for pw := range workQueue {
if !c.Bitfield.HasPiece(pw.index) { if !c.Bitfield.HasPiece(pw.index) {
workQueue <- pw // Put piece back on the queue workQueue <- pw // Put piece back on the queue
continue continue
} }
// Download the piece // Download the piece
buf, err := attemptDownloadPiece(c, pw) buf, err := attemptDownloadPiece(c, pw)
if err != nil { if err != nil {
log.Println("Exiting", err) log.Println("Exiting", err)
workQueue <- pw // Put piece back on the queue workQueue <- pw // Put piece back on the queue
return return
} }
err = checkIntegrity(pw, buf) err = checkIntegrity(pw, buf)
if err != nil { if err != nil {
log.Printf("Piece #%d failed integrity check\n", pw.index) log.Printf("Piece #%d failed integrity check\n", pw.index)
workQueue <- pw // Put piece back on the queue workQueue <- pw // Put piece back on the queue
continue continue
} }
c.SendHave(pw.index) c.SendHave(pw.index)
results <- &pieceResult{pw.index, buf} 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 ```go
type pieceProgress struct { type pieceProgress struct {
index int index int
client *client.Client client *client.Client
buf []byte buf []byte
downloaded int downloaded int
requested int requested int
backlog int backlog int
} }
func (state *pieceProgress) readMessage() error { func (state *pieceProgress) readMessage() error {
msg, err := state.client.Read() // this call blocks msg, err := state.client.Read() // this call blocks
switch msg.ID { switch msg.ID {
case message.MsgUnchoke: case message.MsgUnchoke:
state.client.Choked = false state.client.Choked = false
case message.MsgChoke: case message.MsgChoke:
state.client.Choked = true state.client.Choked = true
case message.MsgHave: case message.MsgHave:
index, err := message.ParseHave(msg) index, err := message.ParseHave(msg)
state.client.Bitfield.SetPiece(index) state.client.Bitfield.SetPiece(index)
case message.MsgPiece: case message.MsgPiece:
n, err := message.ParsePiece(state.index, state.buf, msg) n, err := message.ParsePiece(state.index, state.buf, msg)
state.downloaded += n state.downloaded += n
state.backlog-- state.backlog--
} }
return nil return nil
} }
``` ```
@ -523,43 +523,43 @@ const MaxBlockSize = 16384
const MaxBacklog = 5 const MaxBacklog = 5
func attemptDownloadPiece(c *client.Client, pw *pieceWork) ([]byte, error) { func attemptDownloadPiece(c *client.Client, pw *pieceWork) ([]byte, error) {
state := pieceProgress{ state := pieceProgress{
index: pw.index, index: pw.index,
client: c, client: c,
buf: make([]byte, pw.length), buf: make([]byte, pw.length),
} }
// Setting a deadline helps get unresponsive peers unstuck. // Setting a deadline helps get unresponsive peers unstuck.
// 30 seconds is more than enough time to download a 262 KB piece // 30 seconds is more than enough time to download a 262 KB piece
c.Conn.SetDeadline(time.Now().Add(30 * time.Second)) c.Conn.SetDeadline(time.Now().Add(30 * time.Second))
defer c.Conn.SetDeadline(time.Time{}) // Disable the deadline defer c.Conn.SetDeadline(time.Time{}) // Disable the deadline
for state.downloaded < pw.length { for state.downloaded < pw.length {
// If unchoked, send requests until we have enough unfulfilled requests // If unchoked, send requests until we have enough unfulfilled requests
if !state.client.Choked { if !state.client.Choked {
for state.backlog < MaxBacklog && state.requested < pw.length { for state.backlog < MaxBacklog && state.requested < pw.length {
blockSize := MaxBlockSize blockSize := MaxBlockSize
// Last block might be shorter than the typical block // Last block might be shorter than the typical block
if pw.length-state.requested < blockSize { if pw.length-state.requested < blockSize {
blockSize = pw.length - state.requested blockSize = pw.length - state.requested
} }
err := c.SendRequest(pw.index, state.requested, blockSize) err := c.SendRequest(pw.index, state.requested, blockSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
state.backlog++ state.backlog++
state.requested += blockSize state.requested += blockSize
} }
} }
err := state.readMessage() err := state.readMessage()
if err != nil { if err != nil {
return nil, err 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 package main
import ( import (
"log" "log"
"os" "os"
"github.com/veggiedefender/torrent-client/torrentfile" "github.com/veggiedefender/torrent-client/torrentfile"
) )
func main() { func main() {
inPath := os.Args[1] inPath := os.Args[1]
outPath := os.Args[2] outPath := os.Args[2]
tf, err := torrentfile.Open(inPath) tf, err := torrentfile.Open(inPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
err = tf.DownloadToFile(outPath) err = tf.DownloadToFile(outPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
``` ```

Loading…
Cancel
Save