Quickstart Guide

Get started with the Merple API in four simple steps: upload, get info, download, and delete files.

(The API is plain HTTP, meaning it's language agnostic. We will use whatever programming language you choose on the top left)

Prerequisites

  • • Get your shard node URL and API key from your "Shard Info" page (go to one of your shard's pages and you will see an info icon which will lead you to this page)
  • • All paths must be absolute (start with /)
  • • Include x-user-api-key header in all requests

Setup

Constants

const API_KEY = "YOUR-API-KEY"
const SHARD_NODE_URL = "https://YOUR-NODE-URL"
const API_KEY = "YOUR-API-KEY"
const SHARD_NODE_URL = "https://YOUR-NODE-URL"

1. Upload File

POST /fso?path=/your/file/path.txt

Upload Function


func UploadFile(localFilePath string, pathOnShard string) {
	f, err := os.Open(localFilePath)
	if err != nil {
		panic(fmt.Errorf("could not open file: %w", err))
	}
	defer f.Close()
	
	url := fmt.Sprintf("%s/fso?path=%s", SHARD_NODE_URL, url.QueryEscape(pathOnShard))
	req, err := http.NewRequest(http.MethodPost, url, f)
	if err != nil {
		panic(fmt.Errorf("could not create upload request: %w", err))
	}
	
	req.Header.Set("x-user-api-key", API_KEY)
	req.Header.Set("Content-Type", "application/octet-stream")

	client := http.DefaultClient
	resp, err := client.Do(req)
	if err != nil {
		panic(fmt.Errorf("error uploading file: %w", err))
	}
	defer resp.Body.Close()
	
	if resp.StatusCode != http.StatusCreated {
		body, _ := io.ReadAll(resp.Body)
		panic(fmt.Sprintf("Upload failed with status %d: %s", resp.StatusCode, string(body)))
	}
}
// example usage: UploadFile("./test.txt", "/files/test.txt")
function uploadFile(localPath: string, pathOnShard: string): Promise<void> {
    const stats = fs.statSync(localPath);
    const stream = fs.createReadStream(localPath);

    const url = new URL(`${SHARD_URL}/fso`);
    url.searchParams.set('path', pathOnShard);

    return new Promise((resolve, reject) => {
        const req = http.request({
            hostname: url.hostname,
            port: url.port,
            path: url.pathname + url.search,
            method: 'POST',
            headers: {
                'x-user-api-key': API_KEY,
                'Content-Type': 'application/octet-stream',
                'Content-Length': stats.size
            }
        }, (res) => {
            let body = '';
            res.on('data', chunk => body += chunk);
            res.on('end', () => {
                res.statusCode === 201 ? resolve() : reject(new Error(`${res.statusCode}: ${body}`));
            });
        });

        req.on('error', reject);
        stream.on('error', reject);
        stream.pipe(req);
    });
}
// example usage: uploadFile("./test.txt", "/files/test.txt");

2. Get File Info

GET /fso-info?path=/your/file/path.txt

Get Info Function

type EntryInfo struct {
	Path         string      `json:"path"`
	Name         string      `json:"name"`
	Size         int64       `json:"size"`
	LastModified string      `json:"last_modified"`
	Nested       []EntryInfo `json:"nested"`
}

func GetFSOInfo(pathOnShard string) *EntryInfo {
	info := &EntryInfo{}
	url := fmt.Sprintf("%s/fso-info?path=%s", SHARD_NODE_URL, url.QueryEscape(pathOnShard))
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		panic(fmt.Errorf("could not create request: %w", err))
	}

	req.Header.Set("x-user-api-key", API_KEY)

	client := http.DefaultClient
	resp, err := client.Do(req)
	if err != nil {
		panic(fmt.Errorf("error getting file info: %w", err))
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		panic(fmt.Sprintf("Get file info failed with status %d: %s", resp.StatusCode, string(body)))
	}

	err = json.NewDecoder(resp.Body).Decode(info)
	if err != nil {
		panic(fmt.Errorf("could not decode JSON: %w", err))
	}

	return info
}
interface EntryInfo {
    path: string;
    name: string;
    size: number;
    last_modified: string;
    nested: EntryInfo[];
}

async function getFSOInfo(pathOnShard: string): Promise<EntryInfo> {
    const url = `${SHARD_NODE_URL}/fso-info?path=${encodeURIComponent(pathOnShard)}`;
    const res = await fetch(url, {
        method: 'GET',
        headers: { 'x-user-api-key': API_KEY }
    });

    if (res.status !== 200) {
        const body = await res.text();
        throw new Error(`Get file info failed with status ${res.status}: ${body}`);
    }

    const info: EntryInfo = await res.json();
    return info;
}
// example usage: getFSOInfo("/files/test.txt").then((info) => console.log(info))

3. Download File

GET /fso?path=/your/file/path.txt

Download Function


func DownloadFile(pathOnShard string, localFilePath string) {
	url := fmt.Sprintf("%s/fso?path=%s", SHARD_NODE_URL, url.QueryEscape(pathOnShard))
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		panic(fmt.Errorf("could not create download request: %w", err))
	}

	req.Header.Set("x-user-api-key", API_KEY)

	client := http.DefaultClient
	resp, err := client.Do(req)
	if err != nil {
		panic(fmt.Errorf("error downloading file: %w", err))
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		panic(fmt.Sprintf("Download failed with status %d: %s", resp.StatusCode, string(body)))
	}
	
	file, err := os.Create(localFilePath)
	if err != nil {
		panic(fmt.Sprintf("Could not create file: %w", err))
	}
	defer file.Close()
	
	_, err = io.Copy(file, resp.Body)
	if err != nil {
		panic(fmt.Sprintf("Could not save file: %w", err))
	}
}

async function downloadFile(pathOnShard: string, localFilePath: string): Promise<void>{
  const url = `${SHARD_NODE_URL}/fso?path=${encodeURIComponent(pathOnShard)}`;
  const res = await fetch(url, {
      method: 'GET',
      headers: { 'x-user-api-key': API_KEY }
  });

  if (res.status !== 200) {
      const body = await res.text();
      throw new Error(`Download failed with status ${res.status}: ${body}`);
  }

  const data = await res.arrayBuffer();
  await fs.promises.writeFile(localFilePath, new Uint8Array(data));
}

4. Delete File / Folder

DELETE /fso?path=/your/file/path.txt

Delete Function


func DeleteFSO(pathOnShard string) {
	url := fmt.Sprintf("%s/fso?path=%s", SHARD_NODE_URL, url.QueryEscape(pathOnShard))
	req, err := http.NewRequest(http.MethodDelete, url, nil)
	if err != nil {
		panic(fmt.Errorf("could not create delete request: %w", err))
	}
	
	req.Header.Set("x-user-api-key", API_KEY)

	client := http.DefaultClient
	resp, err := client.Do(req)
	if err != nil {
		panic(fmt.Errorf("error deleting file: %w", err))
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		panic(fmt.Sprintf("Delete failed with status %d: %s", resp.StatusCode, string(body)))
}
}

async function deleteFile(pathOnShard: string): Promise<void> {
  const url = `${SHARD_NODE_URL}/fso?path=${encodeURIComponent(pathOnShard)}`;
  const res = await fetch(url, {
      method: 'DELETE',
      headers: { 'x-user-api-key': API_KEY }
  });

  if (res.status !== 200) {
      const body = await res.text();
      throw new Error(`Delete failed with status ${res.status}: ${body}`);
  }
}

Complete Example

Main Function


func main() {
	localFile := "hello.txt"
	pathOnShard := "/files/hello.txt"
	downloadPath := "downloaded-hello.txt"
	
	UploadFile(localFile, pathOnShard)
	
	info := GetFSOInfo(pathOnShard)
	fmt.Printf("File: %s (%d bytes)\n", info.Name, info.Size)
	
	DownloadFile(pathOnShard, downloadPath)
	
	DeleteFile(pathOnShard)
}

async function main() {
    const localFile = "hello.txt";
    const pathOnShard = "/files/hello.txt";
    const downloadPath = "downloaded-hello.txt";

    await uploadFile(localFile, pathOnShard);

    const info = await getFSOInfo(pathOnShard);
    console.log(`File: ${info.name} (${info.size} bytes)`);

    await downloadFile(pathOnShard, downloadPath);

    await deleteFile(pathOnShard);
}
main().then(() => console.log("done"))

Response Status Codes

Code Status Description
201 Success Operation completed successfully
400 Bad Request Invalid request parameters
401 Unauthorized Invalid or missing API key
404 Not Found File or path doesn't exist
409 Conflict File already exists (use overwrite=true)