From 93a4f7a249e0641df118181185f6b2de78051d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20S=C3=A9n=C3=A9chal?= <44696378+thomas-senechal@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:58:31 +0200 Subject: [PATCH] 10 create get fct to retrieve website on the blockchain (#45) * Add Fetch and GetOwner functions to website pkg * Add zipper pkg with GetFileFromZip function * Update CLI main to show owner and index.html from downloaded website * Update .gitignore to exclude website archives * Improve Chunk tests and add some test cases * Remove useless error of prepareUploadParams function * Update website read.go to use FinalValue instead of CandidateValue --- .gitignore | 3 ++ cmd/cli/main.go | 35 ++++++++++++++++++ int/zipper/zipper.go | 36 ++++++++++++++++++ pkg/website/chunk_test.go | 23 +++++++++++- pkg/website/read.go | 77 +++++++++++++++++++++++++++++++++++++++ pkg/website/upload.go | 9 ++--- 6 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 int/zipper/zipper.go create mode 100644 pkg/website/read.go diff --git a/.gitignore b/.gitignore index 3734eeb..71ef95a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,8 @@ build *.log +# Websites archives +*.zip + # MacOS .DS_Store diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 82aef1d..29d6148 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -7,6 +7,7 @@ import ( "github.com/massalabs/DeWeb/int/config" "github.com/massalabs/DeWeb/int/utils" + "github.com/massalabs/DeWeb/int/zipper" pkgConfig "github.com/massalabs/DeWeb/pkg/config" "github.com/massalabs/DeWeb/pkg/website" "github.com/massalabs/station/pkg/logger" @@ -32,6 +33,40 @@ func main() { } logger.Infof("Website uploaded successfully to address: %s", address) + + owner, err := website.GetOwner(&config.NetworkInfos, address) + if err != nil { + logger.Fatalf("failed to get website owner: %v", err) + } + + logger.Infof("Website owner: %s", owner) + + websiteBytes, err := website.Fetch(&config.NetworkInfos, address) + if err != nil { + logger.Fatalf("failed to fetch website: %v", err) + } + + logger.Infof("Website fetched successfully with size: %d", len(websiteBytes)) + + outputZipPath := fmt.Sprintf("website_%s.zip", address) + + err = os.WriteFile(outputZipPath, websiteBytes, 0o644) + if err != nil { + logger.Error("Failed to write website zip file", err) + return + } + + logger.Info("Website successfully written to file: %s", outputZipPath) + + fileName := "index.html" + + content, err := zipper.GetFileFromZip(outputZipPath, fileName) + if err != nil { + logger.Error(err) + return + } + + logger.Infof("%s content:\n %s", fileName, content) } func deployWebsite(config *pkgConfig.Config) (string, error) { diff --git a/int/zipper/zipper.go b/int/zipper/zipper.go new file mode 100644 index 0000000..fa7ac24 --- /dev/null +++ b/int/zipper/zipper.go @@ -0,0 +1,36 @@ +package zipper + +import ( + "archive/zip" + "fmt" + "io" +) + +// GetFileFromZip returns the content of the given file from the zip file. +// It returns an error if the file is not found in the zip. +func GetFileFromZip(zipFilePath, fileName string) ([]byte, error) { + zipReader, err := zip.OpenReader(zipFilePath) + if err != nil { + return []byte{}, fmt.Errorf("failed to open zip file: %w", err) + } + defer zipReader.Close() + + for _, file := range zipReader.File { + if file.Name == fileName { + rc, err := file.Open() + if err != nil { + return []byte{}, fmt.Errorf("failed to open file in zip: %w", err) + } + defer rc.Close() + + buf, err := io.ReadAll(rc) + if err != nil { + return []byte{}, fmt.Errorf("failed to read file in zip: %w", err) + } + + return buf, nil + } + } + + return []byte{}, fmt.Errorf("%s not found in zip", fileName) +} diff --git a/pkg/website/chunk_test.go b/pkg/website/chunk_test.go index 00a1745..c98f3ab 100644 --- a/pkg/website/chunk_test.go +++ b/pkg/website/chunk_test.go @@ -1,6 +1,7 @@ package website import ( + _ "embed" "testing" "github.com/stretchr/testify/assert" @@ -18,6 +19,10 @@ func TestChunk(t *testing.T) { {"Median", []byte("Hello, World!"), 4}, {"Big byte array", []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), 16}, {"Small byte array", []byte("hello"), 32}, + {"Single character", []byte("a"), 2}, + {"Exact divisible length", []byte("123456"), 2}, + {"Long sentence", []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."), 3}, + {"Website deployer wasm", sc, ChunkSize}, } for _, test := range tests { @@ -26,13 +31,27 @@ func TestChunk(t *testing.T) { if test.data == nil || test.chunkSize <= 0 { assert.Nil(t, chunks) - return } expectedLen := (len(test.data) + test.chunkSize - 1) / test.chunkSize - assert.Equal(t, len(chunks), expectedLen) + + expectedLastChunkLen := len(test.data) % test.chunkSize + if expectedLastChunkLen == 0 && len(test.data) > 0 { + expectedLastChunkLen = test.chunkSize + } + + // Assert the length of each chunk except the last one + for i := 0; i < len(chunks)-1; i++ { + assert.Equal(t, test.chunkSize, len(chunks[i]), "Chunk size does not match the expected value.") + } + + // Assert the length of the last chunk + if len(chunks) > 0 { + lastChunk := chunks[len(chunks)-1] + assert.Equal(t, expectedLastChunkLen, len(lastChunk), "The length of the last chunk does not match the expected value.") + } }) } } diff --git a/pkg/website/read.go b/pkg/website/read.go new file mode 100644 index 0000000..40e54ab --- /dev/null +++ b/pkg/website/read.go @@ -0,0 +1,77 @@ +package website + +import ( + "fmt" + + "github.com/massalabs/station/int/config" + "github.com/massalabs/station/pkg/convert" + "github.com/massalabs/station/pkg/node" +) + +// Fetch retrieves the complete data of a website as bytes. +func Fetch(network *config.NetworkInfos, websiteAddress string) ([]byte, error) { + client := node.NewClient(network.NodeURL) + + chunkNumber, err := getNumberOfChunks(client, websiteAddress) + if err != nil { + return nil, fmt.Errorf("fetching number of chunks: %w", err) + } + + dataStore, err := fetchAllChunks(client, websiteAddress, chunkNumber) + if err != nil { + return nil, fmt.Errorf("fetching all chunks: %w", err) + } + + return dataStore, nil +} + +// getNumberOfChunks fetches and returns the number of chunks for the website. +func getNumberOfChunks(client *node.Client, websiteAddress string) (int32, error) { + nbChunkResponse, err := node.FetchDatastoreEntry(client, websiteAddress, convert.ToBytes(nbChunkKey)) + if err != nil { + return 0, fmt.Errorf("fetching website number of chunks: %w", err) + } + + chunkNumber, err := convert.BytesToI32(nbChunkResponse.FinalValue) + if err != nil { + return 0, fmt.Errorf("converting fetched data for key '%s': %w", nbChunkKey, err) + } + + return chunkNumber, nil +} + +// fetchAllChunks retrieves all chunks of data for the website. +func fetchAllChunks(client *node.Client, websiteAddress string, chunkNumber int32) ([]byte, error) { + keys := make([][]byte, chunkNumber) + for i := 0; i < int(chunkNumber); i++ { + keys[i] = convert.I32ToBytes(i) + } + + response, err := node.ContractDatastoreEntries(client, websiteAddress, keys) + if err != nil { + return nil, fmt.Errorf("calling get_datastore_entries '%+v': %w", keys, err) + } + + if len(response) != int(chunkNumber) { + return nil, fmt.Errorf("expected %d entries, got %d", chunkNumber, len(response)) + } + + var dataStore []byte + for _, entry := range response { + dataStore = append(dataStore, entry.FinalValue...) + } + + return dataStore, nil +} + +// GetOwner retrieves the owner of the website. +func GetOwner(network *config.NetworkInfos, websiteAddress string) (string, error) { + client := node.NewClient(network.NodeURL) + + ownerResponse, err := node.FetchDatastoreEntry(client, websiteAddress, convert.ToBytes(ownerKey)) + if err != nil { + return "", fmt.Errorf("fetching website owner: %w", err) + } + + return string(ownerResponse.FinalValue), nil +} diff --git a/pkg/website/upload.go b/pkg/website/upload.go index c92b417..7fff8e3 100644 --- a/pkg/website/upload.go +++ b/pkg/website/upload.go @@ -30,10 +30,7 @@ func UploadChunk( return "", fmt.Errorf("chunk is empty, no data to upload") } - params, err := prepareUploadParams(chunk, chunkIndex) - if err != nil { - return "", fmt.Errorf("preparing upload params: %w", err) - } + params := prepareUploadParams(chunk, chunkIndex) uploadCost, err := ComputeChunkCost(chunkIndex, len(chunk)) if err != nil { @@ -45,12 +42,12 @@ func UploadChunk( return performUpload(config, websiteAddress, params, uploadCost, chunkIndex) } -func prepareUploadParams(chunk []byte, chunkIndex int) ([]byte, error) { +func prepareUploadParams(chunk []byte, chunkIndex int) []byte { params := convert.I32ToBytes(chunkIndex) params = append(params, convert.U32ToBytes(len(chunk))...) params = append(params, chunk...) - return params, nil + return params } func performUpload(