Files
tiny-forge/internal/docker/volume.go
alexei.dolgolyov b622384774
Build / build (push) Successful in 10m21s
feat: persistent storage for Deno static sites
- 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.
2026-04-13 00:12:51 +03:00

84 lines
2.2 KiB
Go

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
}