- Add storage_enabled and storage_limit_mb columns to static_sites.
- Create/attach Docker volumes (tinyforge-site-{name}-data) for Deno
sites with storage enabled, mounted at /app/data.
- Grant --allow-write=/app/data in Deno container CMD.
- Add storage usage API endpoint (GET /api/sites/{id}/storage).
- Show storage section in site detail page with usage bar.
- Add storage toggle and limit field to new site wizard.
- Use ConfirmDialog for secret deletion instead of inline delete.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
// ExecInContainer runs a command inside a running container and returns the combined output.
|
||||
func (c *Client) ExecInContainer(ctx context.Context, containerID string, cmd []string) (string, error) {
|
||||
exec, err := c.api.ExecCreate(ctx, containerID, client.ExecCreateOptions{
|
||||
Cmd: cmd,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec create: %w", err)
|
||||
}
|
||||
|
||||
attach, err := c.api.ExecAttach(ctx, exec.ID, client.ExecAttachOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec attach: %w", err)
|
||||
}
|
||||
defer attach.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, attach.Reader); err != nil {
|
||||
return "", fmt.Errorf("exec read output: %w", err)
|
||||
}
|
||||
|
||||
// Check exit code.
|
||||
inspect, err := c.api.ExecInspect(ctx, exec.ID, client.ExecInspectOptions{})
|
||||
if err != nil {
|
||||
return buf.String(), nil // best effort — return output even if inspect fails
|
||||
}
|
||||
if inspect.ExitCode != 0 {
|
||||
return "", fmt.Errorf("exec exited with code %d", inspect.ExitCode)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
// SiteVolumeName returns the deterministic volume name for a static site.
|
||||
func SiteVolumeName(siteName string) string {
|
||||
return fmt.Sprintf("tinyforge-site-%s-data", siteName)
|
||||
}
|
||||
|
||||
// EnsureSiteVolume creates a named volume for a static site if it doesn't already exist.
|
||||
// Returns the volume name.
|
||||
func (c *Client) EnsureSiteVolume(ctx context.Context, siteName string) (string, error) {
|
||||
name := SiteVolumeName(siteName)
|
||||
|
||||
// Check if volume already exists.
|
||||
_, err := c.api.VolumeInspect(ctx, name, client.VolumeInspectOptions{})
|
||||
if err == nil {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// Create the volume.
|
||||
_, err = c.api.VolumeCreate(ctx, client.VolumeCreateOptions{
|
||||
Name: name,
|
||||
Labels: map[string]string{
|
||||
"tinyforge.static-site": "true",
|
||||
"tinyforge.site-name": siteName,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create volume %s: %w", name, err)
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// RemoveSiteVolume removes the named volume for a static site.
|
||||
func (c *Client) RemoveSiteVolume(ctx context.Context, siteName string) error {
|
||||
name := SiteVolumeName(siteName)
|
||||
_, err := c.api.VolumeRemove(ctx, name, client.VolumeRemoveOptions{Force: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove volume %s: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StorageUsage holds the disk usage info for a site volume.
|
||||
type StorageUsage struct {
|
||||
UsedBytes int64 `json:"used_bytes"`
|
||||
}
|
||||
|
||||
// InspectSiteStorageUsage execs `du -sb /app/data` inside the container to get usage.
|
||||
// Returns 0 if the container is not running or the path doesn't exist.
|
||||
func (c *Client) InspectSiteStorageUsage(ctx context.Context, containerID string) (StorageUsage, error) {
|
||||
if containerID == "" {
|
||||
return StorageUsage{}, nil
|
||||
}
|
||||
|
||||
output, err := c.ExecInContainer(ctx, containerID, []string{"du", "-sb", "/app/data"})
|
||||
if err != nil {
|
||||
// Container not running or path doesn't exist — return zero.
|
||||
return StorageUsage{}, nil
|
||||
}
|
||||
|
||||
// du output: "12345\t/app/data\n"
|
||||
parts := strings.Fields(strings.TrimSpace(output))
|
||||
if len(parts) == 0 {
|
||||
return StorageUsage{}, nil
|
||||
}
|
||||
|
||||
bytes, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return StorageUsage{}, nil
|
||||
}
|
||||
|
||||
return StorageUsage{UsedBytes: bytes}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user