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
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)
}
```

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

@ -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)
// 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)
}
}
```

Loading…
Cancel
Save