Console API
JuiceFS Web Console provides API for all file system interactions that a user can perform through a web browser, e.g. create file systems, browse file system, check the trash directory or configure client ACL.
Since the Web Console does not provide file system modification capabilities, you cannot do that with the APIs as well. If you need to programmatically modify your file systems, use S3 Gateway.
This chapter only introduces the API authentication process, check the API schema to learn the specifics.
Usage
Code example
- Python
- Go
import base64
import hashlib
import hmac
import json
import time
from typing import Dict, List, Literal, Optional, Union
from urllib.parse import quote_plus, urlencode, urlsplit
import requests
access_key = 'ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925'
secret_key = '5f0c5a5d51515947788fa7b8244acebe166aedd9de28b26ef716888a613c3d92'
def sign(
secret_key: str,
timestamp: int,
method: Literal['GET', 'POST', 'PATCH', 'PUT', 'DELETE'],
path: str,
headers: Dict[str, str],
query_params: Optional[Dict[str, Union[str, List[str]]]],
body: Optional[bytes],
) -> str:
sorted_headers = []
for h in ['Host']:
v = headers.get(h, '')
if not v:
raise ValueError(f'header {h} is required')
sorted_headers.append(f'{h.lower()}:{v}')
sorted_headers = '\n'.join(sorted_headers)
sorted_qs = ''
if query_params:
sorted_qs = sorted(query_params.items(), key=lambda item: item[0])
for i, (k, values) in enumerate(sorted_qs):
if not isinstance(values, list):
values = [values]
sorted_qs[i] = (k, sorted(values))
sorted_qs = urlencode(sorted_qs, doseq=True, safe='', quote_via=quote_plus)
payload_hash = hashlib.sha256(body).hexdigest() if body else ''
parts = [str(timestamp), method, path, sorted_headers, sorted_qs, payload_hash]
data = '\n'.join(parts)
signature = hmac.new(
secret_key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256
).hexdigest()
return signature
def request(
method: Literal['GET', 'POST', 'PATCH', 'PUT', 'DELETE'],
url: str,
query_params: Optional[dict] = None,
data: Optional[dict] = None,
):
timestamp = int(time.time())
parts = urlsplit(url)
path = parts.path
headers = {'Host': parts.netloc}
req = requests.Request(method, url, params=query_params, json=data).prepare()
body = req.body or b''
print(f'secret_key: {secret_key[:10]}...{secret_key[-10:]}')
print(f'timestamp: {timestamp}')
print(f'method: {method}')
print(f'path: {path}')
print(f'headers: {headers}')
print(f'query_params: {query_params if query_params else "empty"}')
print(f'body: {body if body else "empty"}')
signature = sign(secret_key, timestamp, method, path, headers, query_params, body)
print(f'signature: {signature}')
auth = {
'access_key': access_key,
'timestamp': timestamp,
'signature': signature,
'version': 1,
}
token = base64.b64encode(json.dumps(auth).encode()).decode()
print(f'token: {token}')
req.headers['Authorization'] = token
resp = requests.Session().send(req)
print()
print(json.dumps(resp.json(), indent=2))
# API schema doc: https://juicefs.com/api/v1/docs
API_URL = 'http://localhost:8080/api/v1'
def get_volumes():
url = f'{API_URL}/volumes'
request('GET', url)
def delete_volume():
url = f'{API_URL}/volumes/1'
request('DELETE', url)
def get_volume_exports():
url = f'{API_URL}/volumes/1/exports'
request('GET', url)
def create_volume_export():
url = f'{API_URL}/volumes/1/exports'
request(
'POST',
url,
data={
'desc': 'for mount',
'iprange': '192.168.0.1/24',
'apionly': False,
'readonly': False,
'appendonly': False,
},
)
def update_volume_export():
url = f'{API_URL}/volumes/1/exports/1'
request('PUT', url, data={'desc': 'for mount', 'iprange': '192.168.100.1/24'})
def get_volume_quotas():
url = f'{API_URL}/volumes/1/quotas'
request('GET', url)
def create_volume_quota():
url = f'{API_URL}/volumes/1/quotas'
request(
'POST',
url,
data={'path': '/path/to/subdir', 'inodes': 1 << 20, 'size': 1 << 30},
)
def update_volume_quota():
url = f'{API_URL}/volumes/1/quotas/9'
request('PUT', url, data={'path': '/foo', 'size': 10 << 30})
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
const (
API_URL = "http://localhost:8080/api/v1"
ACCESS_KEY = "ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925"
SECRET_KEY = "5f0c5a5d51515947788fa7b8244acebe166aedd9de28b26ef716888a613c3d92"
)
func sign(
secretKey string,
timestamp int64,
method string,
path string,
headers http.Header,
queryParams url.Values,
body []byte,
) (string, error) {
sortedHeaders := []string{}
for _, h := range []string{"Host"} {
v := headers.Get(h)
if v == "" {
return "", fmt.Errorf("header %s is required", h)
}
sortedHeaders = append(sortedHeaders, fmt.Sprintf("%s:%s", strings.ToLower(h), v))
}
srotedHeadersString := strings.Join(sortedHeaders, "\n")
sortedQueryString := ""
if queryParams != nil {
sortedKeys := make([]string, 0, len(queryParams))
for k := range queryParams {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
sortedQueryParams := make([]string, 0, len(queryParams))
for _, k := range sortedKeys {
sort.Strings(queryParams[k])
for _, value := range queryParams[k] {
sortedQueryParams = append(sortedQueryParams, fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(value)))
}
}
sortedQueryString = strings.Join(sortedQueryParams, "&")
}
payloadHash := ""
if body != nil {
hash := sha256.New()
hash.Write(body)
payloadHash = hex.EncodeToString(hash.Sum(nil))
}
parts := []string{
fmt.Sprintf("%d", timestamp),
method,
path,
srotedHeadersString,
sortedQueryString,
payloadHash,
}
data := strings.Join(parts, "\n")
hash := hmac.New(sha256.New, []byte(secretKey))
hash.Write([]byte(data))
signature := hex.EncodeToString(hash.Sum(nil))
return signature, nil
}
func request(
method string,
api_url string,
queryParams url.Values,
data map[string]interface{},
) error {
var (
body []byte
err error
json_data []byte
)
timestamp := time.Now().Unix()
api_url = fmt.Sprintf("%s?%s", api_url, queryParams.Encode())
if data != nil {
body, err = json.Marshal(data)
if err != nil {
return err
}
}
req, err := http.NewRequest(method, api_url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Host", req.URL.Host)
fmt.Printf("secret_key: %s...%s\n", SECRET_KEY[:10], SECRET_KEY[len(SECRET_KEY)-10:])
fmt.Printf("timestamp: %d\n", timestamp)
fmt.Printf("method: %s\n", method)
fmt.Printf("path: %s\n", req.URL.Path)
json_data, err = json.Marshal(req.Header)
if err != nil {
return err
}
fmt.Printf("headers: %s\n", string(json_data))
fmt.Printf("query_params: %v\n", req.URL.RawQuery)
fmt.Printf("body: %s\n", body)
signature, err := sign(SECRET_KEY, timestamp, method, req.URL.Path, req.Header, queryParams, body)
if err != nil {
return err
}
fmt.Printf("signature: %s\n", signature)
auth := map[string]interface{}{
"access_key": ACCESS_KEY,
"timestamp": timestamp,
"signature": signature,
"version": 1,
}
jsonString, err := json.Marshal(auth)
if err != nil {
return err
}
token := base64.StdEncoding.EncodeToString(jsonString)
fmt.Printf("token: %s\n", token)
req.Header.Set("Authorization", token)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
resp_body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Printf("response: \n%s\n", string(resp_body))
return nil
}
func getVolumes() error {
u := fmt.Sprintf("%s/volumes", API_URL)
return request("GET", u, nil, nil)
}
func deleteVolume() error {
u := fmt.Sprintf("%s/volumes/1", API_URL)
return request("DELETE", u, nil, nil)
}
func getVolumeExports() error {
u := fmt.Sprintf("%s/volumes/1/exports", API_URL)
return request("GET", u, nil, nil)
}
func createVolumeExport() error {
u := fmt.Sprintf("%s/volumes/1/exports", API_URL)
return request(
"POST",
u,
nil,
map[string]interface{}{
"desc": "for mount",
"iprange": "192.168.0.1/24",
"apionly": false,
"readonly": false,
"appendonly": false,
},
)
}
func updateVolumeExport() error {
u := fmt.Sprintf("%s/volumes/1/exports/1", API_URL)
return request("PUT", u, nil, map[string]interface{}{"desc": "abc", "iprange": "192.168.100.1/24"})
}
func getVolumeQuotas() error {
u := fmt.Sprintf("%s/volumes/1/quotas", API_URL)
return request("GET", u, nil, nil)
}
func createVolumeQuota() error {
u := fmt.Sprintf("%s/volumes/1/quotas", API_URL)
return request(
"POST",
u,
nil,
map[string]interface{}{"path": "/path/to/subdir", "inodes": 1 << 20, "size": 1 << 30},
)
}
func updateVolumeQuota() error {
u := fmt.Sprintf("%s/volumes/1/quotas/1", API_URL)
return request("PUT", u, nil, map[string]interface{}{"path": "/foo", "size": 10 << 30})
}
func main() {
if err := getVolumes(); err != nil {
fmt.Printf("request error: %s", err)
}
}