package handlers
import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/c4pt0r/agfs/agfs-server/pkg/filesystem"
)
type HandleOpenRequest struct {
Path string `json:"path"`
Flags int `json:"flags"`
Mode uint32 `json:"mode"`
}
type HandleOpenResponse struct {
HandleID int64 `json:"handle_id"`
Path string `json:"path"`
Flags int `json:"flags"`
Lease int `json:"lease"`
ExpiresAt time.Time `json:"expires_at"`
}
type HandleInfoResponse struct {
HandleID int64 `json:"handle_id"`
Path string `json:"path"`
Flags int `json:"flags"`
Lease int `json:"lease"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
LastAccess time.Time `json:"last_access"`
}
type HandleListResponse struct {
Handles []HandleInfoResponse `json:"handles"`
Count int `json:"count"`
Max int `json:"max"`
}
type HandleReadResponse struct {
BytesRead int `json:"bytes_read"`
Position int64 `json:"position"`
}
type HandleWriteResponse struct {
BytesWritten int `json:"bytes_written"`
Position int64 `json:"position"`
}
type HandleSeekResponse struct {
Position int64 `json:"position"`
}
type HandleRenewResponse struct {
ExpiresAt time.Time `json:"expires_at"`
Lease int `json:"lease"`
}
func parseOpenFlags(flagStr string) (filesystem.OpenFlag, error) {
if flagStr == "" {
return filesystem.O_RDONLY, nil
}
num, err := strconv.ParseInt(flagStr, 10, 32)
if err != nil {
return 0, fmt.Errorf("invalid flags parameter: must be a number")
}
return filesystem.OpenFlag(num), nil
}
func (h *Handler) getHandleFS() (filesystem.HandleFS, error) {
handleFS, ok := h.fs.(filesystem.HandleFS)
if !ok {
return nil, fmt.Errorf("filesystem does not support file handles")
}
return handleFS, nil
}
func (h *Handler) OpenHandle(w http.ResponseWriter, r *http.Request) {
handleFS, err := h.getHandleFS()
if err != nil {
writeError(w, http.StatusNotImplemented, err.Error())
return
}
path := r.URL.Query().Get("path")
if path == "" {
writeError(w, http.StatusBadRequest, "path parameter is required")
return
}
flagStr := r.URL.Query().Get("flags")
flags, err := parseOpenFlags(flagStr)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
modeStr := r.URL.Query().Get("mode")
mode := uint32(0644)
if modeStr != "" {
m, err := strconv.ParseUint(modeStr, 8, 32)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid mode parameter")
return
}
mode = uint32(m)
}
handle, err := handleFS.OpenHandle(path, flags, mode)
if err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
response := HandleOpenResponse{
HandleID: handle.ID(),
Path: handle.Path(),
Flags: int(handle.Flags()),
Lease: 60,
ExpiresAt: time.Now().Add(60 * time.Second),
}
writeJSON(w, http.StatusOK, response)
}
func (h *Handler) GetHandle(w http.ResponseWriter, r *http.Request, handleIDStr string) {
handleFS, err := h.getHandleFS()
if err != nil {
writeError(w, http.StatusNotImplemented, err.Error())
return
}
handleID, err := strconv.ParseInt(handleIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid handle ID: must be a number")
return
}
handle, err := handleFS.GetHandle(handleID)
if err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
response := HandleInfoResponse{
HandleID: handle.ID(),
Path: handle.Path(),
Flags: int(handle.Flags()),
Lease: 60,
ExpiresAt: time.Now().Add(60 * time.Second),
CreatedAt: time.Now(),
LastAccess: time.Now(),
}
writeJSON(w, http.StatusOK, response)
}
func (h *Handler) CloseHandle(w http.ResponseWriter, r *http.Request, handleIDStr string) {
handleFS, err := h.getHandleFS()
if err != nil {
writeError(w, http.StatusNotImplemented, err.Error())
return
}
handleID, err := strconv.ParseInt(handleIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid handle ID: must be a number")
return
}
if err := handleFS.CloseHandle(handleID); err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
writeJSON(w, http.StatusOK, SuccessResponse{Message: "handle closed"})
}
func (h *Handler) HandleRead(w http.ResponseWriter, r *http.Request, handleIDStr string) {
handleFS, err := h.getHandleFS()
if err != nil {
writeError(w, http.StatusNotImplemented, err.Error())
return
}
handleID, err := strconv.ParseInt(handleIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid handle ID: must be a number")
return
}
handle, err := handleFS.GetHandle(handleID)
if err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
sizeStr := r.URL.Query().Get("size")
size := int64(4096)
if sizeStr != "" {
s, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid size parameter")
return
}
if s < 0 {
size = 1024 * 1024
} else {
size = s
}
}
offsetStr := r.URL.Query().Get("offset")
var data []byte
var n int
if offsetStr != "" {
offset, err := strconv.ParseInt(offsetStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid offset parameter")
return
}
buf := make([]byte, size)
n, err = handle.ReadAt(buf, offset)
if err != nil && err != io.EOF {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
data = buf[:n]
} else {
buf := make([]byte, size)
n, err = handle.Read(buf)
if err != nil && err != io.EOF {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
data = buf[:n]
}
if h.trafficMonitor != nil && n > 0 {
h.trafficMonitor.RecordRead(int64(n))
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("X-Bytes-Read", strconv.Itoa(n))
w.WriteHeader(http.StatusOK)
w.Write(data)
}
func (h *Handler) HandleWrite(w http.ResponseWriter, r *http.Request, handleIDStr string) {
handleFS, err := h.getHandleFS()
if err != nil {
writeError(w, http.StatusNotImplemented, err.Error())
return
}
handleID, err := strconv.ParseInt(handleIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid handle ID: must be a number")
return
}
handle, err := handleFS.GetHandle(handleID)
if err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
data, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "failed to read request body")
return
}
if h.trafficMonitor != nil && len(data) > 0 {
h.trafficMonitor.RecordWrite(int64(len(data)))
}
var n int
offsetStr := r.URL.Query().Get("offset")
if offsetStr != "" {
offset, err := strconv.ParseInt(offsetStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid offset parameter")
return
}
n, err = handle.WriteAt(data, offset)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
} else {
n, err = handle.Write(data)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
response := HandleWriteResponse{
BytesWritten: n,
}
writeJSON(w, http.StatusOK, response)
}
func (h *Handler) HandleSeek(w http.ResponseWriter, r *http.Request, handleIDStr string) {
handleFS, err := h.getHandleFS()
if err != nil {
writeError(w, http.StatusNotImplemented, err.Error())
return
}
handleID, err := strconv.ParseInt(handleIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid handle ID: must be a number")
return
}
handle, err := handleFS.GetHandle(handleID)
if err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
offsetStr := r.URL.Query().Get("offset")
if offsetStr == "" {
writeError(w, http.StatusBadRequest, "offset parameter is required")
return
}
offset, err := strconv.ParseInt(offsetStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid offset parameter")
return
}
whenceStr := r.URL.Query().Get("whence")
whence := io.SeekStart
if whenceStr != "" {
wh, err := strconv.Atoi(whenceStr)
if err != nil || wh < 0 || wh > 2 {
writeError(w, http.StatusBadRequest, "invalid whence parameter (must be 0, 1, or 2)")
return
}
whence = wh
}
pos, err := handle.Seek(offset, whence)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
response := HandleSeekResponse{
Position: pos,
}
writeJSON(w, http.StatusOK, response)
}
func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request, handleIDStr string) {
handleFS, err := h.getHandleFS()
if err != nil {
writeError(w, http.StatusNotImplemented, err.Error())
return
}
handleID, err := strconv.ParseInt(handleIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid handle ID: must be a number")
return
}
handle, err := handleFS.GetHandle(handleID)
if err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
if err := handle.Sync(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, SuccessResponse{Message: "synced"})
}
func (h *Handler) HandleStat(w http.ResponseWriter, r *http.Request, handleIDStr string) {
handleFS, err := h.getHandleFS()
if err != nil {
writeError(w, http.StatusNotImplemented, err.Error())
return
}
handleID, err := strconv.ParseInt(handleIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid handle ID: must be a number")
return
}
handle, err := handleFS.GetHandle(handleID)
if err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
info, err := handle.Stat()
if err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
response := FileInfoResponse{
Name: info.Name,
Size: info.Size,
Mode: info.Mode,
ModTime: info.ModTime.Format(time.RFC3339Nano),
IsDir: info.IsDir,
Meta: info.Meta,
}
writeJSON(w, http.StatusOK, response)
}
func (h *Handler) HandleStream(w http.ResponseWriter, r *http.Request, handleIDStr string) {
handleFS, err := h.getHandleFS()
if err != nil {
writeError(w, http.StatusNotImplemented, err.Error())
return
}
handleID, err := strconv.ParseInt(handleIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid handle ID: must be a number")
return
}
handle, err := handleFS.GetHandle(handleID)
if err != nil {
status := mapErrorToStatus(err)
writeError(w, status, err.Error())
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Transfer-Encoding", "chunked")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
flusher, ok := w.(http.Flusher)
if !ok {
writeError(w, http.StatusInternalServerError, "streaming not supported")
return
}
buf := make([]byte, 64*1024)
for {
n, err := handle.Read(buf)
if n > 0 {
_, writeErr := w.Write(buf[:n])
if writeErr != nil {
return
}
flusher.Flush()
if h.trafficMonitor != nil {
h.trafficMonitor.RecordRead(int64(n))
}
}
if err == io.EOF {
return
}
if err != nil {
return
}
select {
case <-r.Context().Done():
return
default:
}
}
}
func (h *Handler) SetupHandleRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/handles/open", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
h.OpenHandle(w, r)
})
mux.HandleFunc("/api/v1/handles/", func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/handles/")
if path == "open" || strings.HasPrefix(path, "open?") {
return
}
parts := strings.SplitN(path, "/", 2)
if len(parts) == 0 || parts[0] == "" {
if r.Method == http.MethodGet {
h.ListHandles(w, r)
return
}
writeError(w, http.StatusBadRequest, "handle ID required")
return
}
handleID := parts[0]
operation := ""
if len(parts) > 1 {
operation = parts[1]
}
switch operation {
case "":
switch r.Method {
case http.MethodGet:
h.GetHandle(w, r, handleID)
case http.MethodDelete:
h.CloseHandle(w, r, handleID)
default:
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
case "read":
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
h.HandleRead(w, r, handleID)
case "write":
if r.Method != http.MethodPut {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
h.HandleWrite(w, r, handleID)
case "seek":
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
h.HandleSeek(w, r, handleID)
case "sync":
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
h.HandleSync(w, r, handleID)
case "stat":
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
h.HandleStat(w, r, handleID)
case "stream":
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
h.HandleStream(w, r, handleID)
default:
writeError(w, http.StatusNotFound, "unknown operation: "+operation)
}
})
}
func (h *Handler) ListHandles(w http.ResponseWriter, r *http.Request) {
response := HandleListResponse{
Handles: []HandleInfoResponse{},
Count: 0,
Max: 10000,
}
writeJSON(w, http.StatusOK, response)
}