mukan-ignite/ignite/pkg/jsonfile/jsonfile.go
Mukan Erkin Törük 26b204bd04
Some checks are pending
Docs Deploy / build_and_deploy (push) Waiting to run
Generate Docs / cli (push) Waiting to run
Generate Config Doc / cli (push) Waiting to run
Go formatting / go-formatting (push) Waiting to run
Check links / markdown-link-check (push) Waiting to run
Integration / pre-test (push) Waiting to run
Integration / test on (push) Blocked by required conditions
Integration / status (push) Blocked by required conditions
Lint / Lint Go code (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
feat: fork Ignite CLI v29 as Mukan Ignite — remove cosmos-sdk restrictions
2026-05-11 03:31:37 +03:00

362 lines
8.9 KiB
Go

package jsonfile
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/buger/jsonparser"
"github.com/ignite/cli/v29/ignite/pkg/errors"
"github.com/ignite/cli/v29/ignite/pkg/safeconverter"
"github.com/ignite/cli/v29/ignite/pkg/tarball"
)
const (
keySeparator = "."
)
var (
// ErrFieldNotFound parameter not found into json.
ErrFieldNotFound = errors.New("JSON field not found")
// ErrInvalidValueType invalid value type.
ErrInvalidValueType = errors.New("invalid value type")
// ErrInvalidURL invalid file URL.
ErrInvalidURL = errors.New("invalid file URL")
)
type (
// JSONFile represents the JSON file and also implements the io.write interface,
// saving directly to the file.
JSONFile struct {
file ReadWriteSeeker
tarballPath string
url string
updates map[string][]byte
cache []byte
}
// UpdateFileOption configures file update function with key and value.
UpdateFileOption func(map[string][]byte)
)
type (
// writeTruncate represents the truncate method from io.WriteSeeker interface.
writeTruncate interface {
Truncate(size int64) error
}
// ReadWriteSeeker represents the owns ReadWriteSeeker interface inherit from io.ReadWriteSeeker.
ReadWriteSeeker interface {
io.ReadWriteSeeker
Close() error
Sync() error
}
)
// New creates a new JSONFile.
func New(file ReadWriteSeeker) *JSONFile {
return &JSONFile{
updates: make(map[string][]byte),
file: file,
}
}
// FromPath parses a JSONFile object from path.
func FromPath(path string) (*JSONFile, error) {
file, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND, 0o600)
if err != nil {
return nil, errors.Wrap(err, "cannot open the file")
}
return New(file), nil
}
// FromURL fetches the file from the given URL and returns its content.
// If tarballFileName is not empty, the URL is interpreted as a tarball file,
// tarballFileName is extracted from it and is returned instead of the URL
// content.
func FromURL(ctx context.Context, url, destPath, tarballFileName string) (*JSONFile, error) {
return fromURL(ctx, url, destPath, tarballFileName, http.DefaultClient)
}
// FromURLWithClient fetches the file using the provided HTTP client.
// If client is nil, http.DefaultClient is used.
func FromURLWithClient(ctx context.Context, url, destPath, tarballFileName string, client *http.Client) (*JSONFile, error) {
if client == nil {
client = http.DefaultClient
}
return fromURL(ctx, url, destPath, tarballFileName, client)
}
func fromURL(ctx context.Context, url, destPath, tarballFileName string, client *http.Client) (*JSONFile, error) {
// TODO create a cache system to avoid download genesis with the same hash again
// Download the file from URL
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, ErrInvalidURL
}
// Remove the old file if exists and create a new one
if err := os.RemoveAll(destPath); err != nil {
return nil, err
}
file, err := os.OpenFile(destPath, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0o600)
if err != nil {
return nil, errors.Wrap(err, "cannot create the file")
}
// Copy the downloaded file to buffer and the opened file
var buf bytes.Buffer
if _, err := io.Copy(file, io.TeeReader(resp.Body, &buf)); err != nil {
return nil, err
}
// Check if the downloaded file is a tarball and extract only the necessary JSON file
var ext bytes.Buffer
tarballPath, err := tarball.ExtractFile(&buf, &ext, tarballFileName)
if err != nil && !errors.Is(err, tarball.ErrNotGzipType) && !errors.Is(err, tarball.ErrInvalidFileName) {
return nil, err
} else if err == nil {
// Erase the tarball bite code from the file and copy the correct one
if err := truncate(file, 0); err != nil {
return nil, err
}
if _, err := io.Copy(file, &ext); err != nil {
return nil, err
}
}
return &JSONFile{
updates: make(map[string][]byte),
file: file,
url: url,
tarballPath: tarballPath,
}, nil
}
// Bytes returns the jsonfile byte array.
func (f *JSONFile) Bytes() ([]byte, error) {
file := f.cache
if file != nil {
return file, nil
}
if err := f.Reset(); err != nil {
return nil, err
}
scanner := bufio.NewScanner(f.file)
for scanner.Scan() {
file = append(file, scanner.Bytes()...)
}
if err := scanner.Err(); err != nil {
return nil, err
}
f.cache = file
return file, nil
}
// Field returns the param by key and the position into byte slice from the file reader.
// Key can be a path to a nested parameters eg: app_state.staking.accounts.
func (f *JSONFile) Field(key string, param interface{}) error {
file, err := f.Bytes()
if err != nil {
return err
}
value, dataType, _, err := jsonparser.Get(file, strings.Split(key, keySeparator)...)
if errors.Is(err, jsonparser.KeyPathNotFoundError) {
return ErrFieldNotFound
} else if err != nil {
return err
}
switch dataType {
case jsonparser.Boolean, jsonparser.Array, jsonparser.Number, jsonparser.Object:
err := json.Unmarshal(value, param)
var unmarshalTypeError *json.UnmarshalTypeError
if errors.As(err, &unmarshalTypeError) {
return ErrInvalidValueType
}
case jsonparser.String:
result, err := jsonparser.ParseString(value)
if err != nil {
return err
}
paramStr, ok := param.(*string)
if ok {
*paramStr = result
break
}
var (
unmarshalTypeError *json.UnmarshalTypeError
syntaxTypeError *json.SyntaxError
)
if err := json.Unmarshal(value, param); errors.As(err, &unmarshalTypeError) ||
errors.As(err, &syntaxTypeError) {
return ErrInvalidValueType
}
case jsonparser.NotExist:
case jsonparser.Null:
case jsonparser.Unknown:
default:
return ErrInvalidValueType
}
return nil
}
// WithKeyValue updates a file value object by key.
func WithKeyValue(key string, value string) UpdateFileOption {
return func(update map[string][]byte) {
update[key] = []byte(`"` + value + `"`)
}
}
// WithKeyValueByte updates a file byte value object by key.
func WithKeyValueByte(key string, value []byte) UpdateFileOption {
return func(update map[string][]byte) {
update[key] = value
}
}
// WithKeyValueTimestamp updates a time value.
func WithKeyValueTimestamp(key string, t int64) UpdateFileOption {
return func(update map[string][]byte) {
formatted := time.Unix(t, 0).UTC().Format(time.RFC3339Nano)
update[key] = []byte(`"` + formatted + `"`)
}
}
// WithKeyValueInt updates a file int value object by key.
func WithKeyValueInt(key string, value int64) UpdateFileOption {
return func(update map[string][]byte) {
update[key] = []byte(strconv.FormatInt(value, 10))
}
}
// WithKeyValueUint updates a file uint value object by key.
func WithKeyValueUint(key string, value uint64) UpdateFileOption {
return WithKeyValueInt(key, safeconverter.ToInt64[uint64](value))
}
// Update updates the file with the new parameters by key.
func (f *JSONFile) Update(opts ...UpdateFileOption) error {
for _, opt := range opts {
opt(f.updates)
}
if err := f.Reset(); err != nil {
return err
}
_, err := io.Copy(f, f.file)
return err
}
// Write implement the write method for io.Writer interface.
func (f *JSONFile) Write(p []byte) (int, error) {
var err error
length := len(p)
for key, value := range f.updates {
p, err = jsonparser.Set(p, value, strings.Split(key, keySeparator)...)
if err != nil {
return 0, err
}
delete(f.updates, key)
}
f.cache = p
err = truncate(f.file, 0)
if err != nil {
return 0, err
}
if err := f.Reset(); err != nil {
return 0, err
}
n, err := f.file.Write(p)
if err != nil {
return n, err
}
if n != len(p) {
return n, io.ErrShortWrite
}
return length, nil
}
// truncate removes the current file content.
func truncate(rws io.WriteSeeker, size int) error {
t, ok := rws.(writeTruncate)
if !ok {
return errors.New("truncate: unable to truncate")
}
return t.Truncate(int64(size))
}
// Close the file.
func (f *JSONFile) Close() error {
return f.file.Close()
}
// URL returns the genesis URL.
func (f *JSONFile) URL() string {
return f.url
}
// TarballPath returns the tarball path.
func (f *JSONFile) TarballPath() string {
return f.tarballPath
}
// Hash returns the hash of the file.
func (f *JSONFile) Hash() (string, error) {
if err := f.Reset(); err != nil {
return "", err
}
h := sha256.New()
if _, err := io.Copy(h, f.file); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// String returns the file string.
func (f *JSONFile) String() (string, error) {
if err := f.Reset(); err != nil {
return "", err
}
data, err := io.ReadAll(f.file)
return string(data), err
}
// Reset sets the offset for the next Read or Write to 0.
func (f *JSONFile) Reset() error {
// TODO find a better way to reset or create a
// read of copy the writer with io.TeeReader
_, err := f.file.Seek(0, 0)
return err
}