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