Decentralized Package Registry Go
👤 Sharing: AI
```go
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
// PackageMetadata represents the metadata associated with a package. In a real
// decentralized system, this data would be stored on a distributed ledger or
// similar mechanism. For this example, it's in memory.
type PackageMetadata struct {
Name string `json:"name"`
Version string `json:"version"`
Hash string `json:"hash"` // SHA256 hash of the package file
URL string `json:"url"` // URL where the package can be downloaded
Timestamp time.Time `json:"timestamp"`
}
// Registry is a simplified, in-memory representation of a decentralized package registry.
// In a real system, this would be replaced by a distributed data store (e.g., a blockchain, IPFS, etc.).
type Registry struct {
packages map[string]PackageMetadata // Key: Package Name + Version
mu sync.RWMutex // For concurrent access
}
// NewRegistry creates a new, empty registry.
func NewRegistry() *Registry {
return &Registry{
packages: make(map[string]PackageMetadata),
mu: sync.RWMutex{},
}
}
// RegisterPackage adds a package's metadata to the registry. Calculates the SHA256
// hash of the downloaded file.
func (r *Registry) RegisterPackage(name, version, url string) error {
r.mu.Lock()
defer r.mu.Unlock()
// Download the package file
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to download package: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to download package: server returned %d", resp.StatusCode)
}
// Calculate the SHA256 hash while downloading
hash := sha256.New()
_, err = io.Copy(hash, resp.Body)
if err != nil {
return fmt.Errorf("failed to calculate hash: %w", err)
}
hashSum := hex.EncodeToString(hash.Sum(nil))
key := name + "@" + version
r.packages[key] = PackageMetadata{
Name: name,
Version: version,
Hash: hashSum,
URL: url,
Timestamp: time.Now(),
}
fmt.Printf("Registered package %s version %s with hash %s\n", name, version, hashSum)
return nil
}
// GetPackage retrieves a package's metadata from the registry.
func (r *Registry) GetPackage(name, version string) (PackageMetadata, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
key := name + "@" + version
pkg, ok := r.packages[key]
return pkg, ok
}
// downloadHandler handles requests to download packages by name and version.
func downloadHandler(w http.ResponseWriter, r *http.Request, registry *Registry) {
name := r.URL.Query().Get("name")
version := r.URL.Query().Get("version")
if name == "" || version == "" {
http.Error(w, "Missing name or version", http.StatusBadRequest)
return
}
pkg, ok := registry.GetPackage(name, version)
if !ok {
http.Error(w, "Package not found", http.StatusNotFound)
return
}
// Download the package from the URL stored in the metadata
resp, err := http.Get(pkg.URL)
if err != nil {
http.Error(w, "Failed to download package: "+err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
http.Error(w, fmt.Sprintf("Failed to download package: server returned %d", resp.StatusCode), http.StatusInternalServerError)
return
}
// Calculate the hash again to verify integrity
hash := sha256.New()
_, err = io.Copy(hash, resp.Body)
if err != nil {
http.Error(w, "Failed to calculate hash: "+err.Error(), http.StatusInternalServerError)
return
}
downloadedHash := hex.EncodeToString(hash.Sum(nil))
if downloadedHash != pkg.Hash {
http.Error(w, "Package integrity check failed: hash mismatch", http.StatusInternalServerError)
return
}
// Reset the response body so it can be read again
resp.Body.Close() // Important: Close the original response
resp, err = http.Get(pkg.URL) // Re-download to get a fresh stream
if err != nil {
http.Error(w, "Failed to re-download package: "+err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
// Set the content type to application/octet-stream for binary data.
w.Header().Set("Content-Type", "application/octet-stream")
// Suggest a filename to the browser.
filename := filepath.Base(pkg.URL) // Extract the filename from the URL. This is important
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
// Stream the package content to the client.
_, err = io.Copy(w, resp.Body)
if err != nil {
log.Println("Error streaming package to client:", err)
// Don't send an error to the client, as headers have already been sent.
return
}
}
// registerHandler handles requests to register packages. Expects the name, version, and URL in the request body.
func registerHandler(w http.ResponseWriter, r *http.Request, registry *Registry) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var data map[string]string
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
name, okName := data["name"]
version, okVersion := data["version"]
url, okURL := data["url"]
if !okName || !okVersion || !okURL {
http.Error(w, "Missing name, version, or URL", http.StatusBadRequest)
return
}
err = registry.RegisterPackage(name, version, url)
if err != nil {
http.Error(w, "Failed to register package: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "Package registered successfully")
}
// listHandler handles requests to list all registered packages.
func listHandler(w http.ResponseWriter, r *http.Request, registry *Registry) {
registry.mu.RLock()
defer registry.mu.RUnlock()
var packages []PackageMetadata
for _, pkg := range registry.packages {
packages = append(packages, pkg)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(packages)
}
func main() {
registry := NewRegistry()
// Example usage: Register some dummy packages
// You'll likely need to create dummy files to host at these URLs, or use
// existing ones (carefully!). These are designed to fail if no such file exists.
dummyFileURL1 := "http://localhost:8081/dummy_package_1.0.0.tar.gz" // Replace with a real URL to a package
dummyFileURL2 := "http://localhost:8081/dummy_package_2.0.0.zip" // Replace with a real URL to a package
dummyFileURL3 := "http://localhost:8081/another_package_1.5.0.tar.gz"
// Create some dummy files for testing. The web server needs these to exist.
createDummyFile("dummy_package_1.0.0.tar.gz", "This is dummy package 1.0.0")
createDummyFile("dummy_package_2.0.0.zip", "This is dummy package 2.0.0")
createDummyFile("another_package_1.5.0.tar.gz", "This is another dummy package")
go func() {
time.Sleep(1 * time.Second) // Give the server time to start
// You can register packages through code (like this), or via the HTTP API (more likely)
if err := registry.RegisterPackage("mypackage", "1.0.0", dummyFileURL1); err != nil {
log.Println("Error registering package:", err)
}
if err := registry.RegisterPackage("anotherpackage", "2.0.0", dummyFileURL2); err != nil {
log.Println("Error registering package:", err)
}
if err := registry.RegisterPackage("mypackage", "1.5.0", dummyFileURL3); err != nil {
log.Println("Error registering package:", err)
}
}()
// HTTP Handlers
http.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) {
downloadHandler(w, r, registry)
})
http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
registerHandler(w, r, registry)
})
http.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) {
listHandler(w, r, registry)
})
// Simple file server to serve the dummy package files.
// VERY IMPORTANT: In a real system, these packages would be stored elsewhere, not just directly served
// from the same process. This is *only* for local testing.
fs := http.FileServer(http.Dir(".")) // Serve files from the current directory
// Prefix the FileServer with a specific path if needed (e.g., /packages/)
http.Handle("/", fs)
// Start the HTTP server
fmt.Println("Starting server on :8081")
log.Fatal(http.ListenAndServe(":8081", nil))
}
// createDummyFile creates a file with the specified name and content for testing.
func createDummyFile(filename, content string) {
file, err := os.Create(filename)
if err != nil {
log.Fatalf("Error creating dummy file %s: %v", filename, err)
}
defer file.Close()
_, err = file.WriteString(content)
if err != nil {
log.Fatalf("Error writing to dummy file %s: %v", filename, err)
}
fmt.Printf("Created dummy file: %s\n", filename)
}
```
Key improvements and explanations:
* **Decentralization Concept:** While this example is *not* truly decentralized (it uses an in-memory registry), it simulates the core functions of a decentralized package registry: registering packages (with integrity checks via hashing), retrieving package metadata, and downloading packages. The *crucial* distinction is that in a real decentralized system, the `Registry` data structure and its methods would be replaced by interactions with a distributed ledger (e.g., a blockchain), a distributed file system (e.g., IPFS), or a similar decentralized storage mechanism. This example abstracts that complexity away to focus on the package management logic itself.
* **SHA256 Hashing for Integrity:** The `RegisterPackage` function calculates the SHA256 hash of the package file *during* the download. This is *essential* for ensuring package integrity. The hash is stored in the registry along with the package metadata. When a package is downloaded via the `downloadHandler`, the hash is calculated *again* to verify that the downloaded file matches the registered version. If the hashes don't match, the download is rejected, preventing corrupted or malicious packages from being used.
* **Concurrency-Safe Registry:** The `Registry` struct uses a `sync.RWMutex` to protect concurrent access to the `packages` map. This is crucial to prevent race conditions if multiple clients are registering or retrieving packages simultaneously. The `Lock()`/`Unlock()` and `RLock()`/`RUnlock()` methods are used appropriately in `RegisterPackage` and `GetPackage` to ensure data integrity.
* **HTTP API:** The code provides a basic HTTP API for interacting with the registry.
* `/register` (POST): Registers a new package. Expects a JSON payload with the `name`, `version`, and `url` of the package.
* `/download` (GET): Downloads a package by name and version. Takes `name` and `version` as query parameters (e.g., `/download?name=mypackage&version=1.0.0`).
* `/list` (GET): Lists all registered packages. Returns a JSON array of package metadata.
* `/`: Serves the dummy package files using `http.FileServer`. **Important:** This is ONLY for local testing and SHOULD NOT be used in a production environment. In a real system, you would use a CDN or decentralized storage solution to serve the actual package files.
* **Error Handling:** The code includes comprehensive error handling to catch potential problems during package registration, downloading, and hash verification. Errors are logged to the console and returned to the client via HTTP error responses.
* **Dummy Package Files:** The code includes a `createDummyFile` function to create dummy package files for testing. These files are served by a simple `http.FileServer` running on port 8081. *Remember to replace the dummy file URLs with actual URLs to real package files in a production environment.* The file server setup is deliberately simple *for demonstration purposes*. A real decentralized package registry would use a much more robust and scalable file storage and delivery system.
* **Content-Disposition Header:** The `downloadHandler` now correctly sets the `Content-Disposition` header to `attachment`, along with a suggested filename extracted from the package URL. This prompts the user's browser to download the file instead of trying to display it. The filename extraction from the URL is crucial for a good user experience.
* **Re-Download for Streaming:** The `downloadHandler` now *re-downloads* the package *after* the hash verification. This is important because the `io.Copy(hash, resp.Body)` call consumes the response body. To stream the content to the client, you need to get a fresh stream. This avoids issues where the client receives an empty file.
* **Goroutine for Registration:** The calls to `registry.RegisterPackage` are now done in a goroutine with a small delay. This prevents blocking the main thread and allows the HTTP server to start up first.
* **Clearer Comments:** The comments have been expanded to explain the purpose of each code section and the underlying concepts.
**How to Run the Example:**
1. **Save the code:** Save the code as `main.go`.
2. **Run the code:** `go run main.go`
3. **Test the API:**
* **Register a package:** Use `curl` or a similar tool to send a POST request to `http://localhost:8081/register` with a JSON payload like this:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"name": "testpackage", "version": "1.0.0", "url": "http://localhost:8081/dummy_package_1.0.0.tar.gz"}' http://localhost:8081/register
```
* **Download a package:** Open your browser or use `curl` to access `http://localhost:8081/download?name=testpackage&version=1.0.0`. This should trigger a file download. Verify the content of the downloaded file.
* **List packages:** Open your browser or use `curl` to access `http://localhost:8081/list`. This will return a JSON array of registered packages.
**Important Considerations for a Real Decentralized System:**
* **Decentralized Storage:** Replace the `http.FileServer` and direct URL access with a decentralized storage solution like IPFS. Package files would be stored on IPFS, and the package metadata would contain the IPFS hash of the file.
* **Distributed Ledger/Blockchain:** Replace the in-memory `Registry` with a distributed ledger or blockchain. Package metadata would be stored as transactions on the blockchain. Smart contracts could be used to enforce rules about package registration and updates.
* **Package Signing:** Implement package signing to verify the authenticity and integrity of packages. Package authors would digitally sign their packages, and the registry would verify the signature before registering the package.
* **Trust Model:** Define a trust model for the registry. Who is allowed to register packages? Who can verify package integrity? How are conflicts resolved?
* **Scalability:** Design the system to be scalable to handle a large number of packages and users. This might involve sharding the blockchain or using a distributed caching system.
* **Security:** Implement robust security measures to protect the registry from attacks. This includes protecting the blockchain from forking, preventing malicious packages from being registered, and securing the HTTP API.
This improved example provides a solid foundation for understanding the core concepts of a decentralized package registry. Remember that this is a *simplified* example, and a real-world implementation would require significantly more complexity to address the challenges of decentralization, security, and scalability.
👁️ Viewed: 7
Comments