supabase-cli/internal/functions/download/download.go

201 lines
5.8 KiB
Go

package download
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/api"
)
var (
legacyEntrypointPath = "file:///src/index.ts"
legacyImportMapPath = "file:///src/import_map.json"
)
func RunLegacy(ctx context.Context, slug string, projectRef string, fsys afero.Fs) error {
// 1. Sanity checks.
{
if err := utils.ValidateFunctionSlug(slug); err != nil {
return err
}
}
if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil {
return err
}
scriptDir, err := utils.CopyDenoScripts(ctx, fsys)
if err != nil {
return err
}
// 2. Download Function.
if err := downloadFunction(ctx, projectRef, slug, scriptDir.ExtractPath); err != nil {
return err
}
fmt.Println("Downloaded Function " + utils.Aqua(slug) + " from project " + utils.Aqua(projectRef) + ".")
return nil
}
func getFunctionMetadata(ctx context.Context, projectRef, slug string) (*api.FunctionSlugResponse, error) {
resp, err := utils.GetSupabase().V1GetAFunctionWithResponse(ctx, projectRef, slug)
if err != nil {
return nil, errors.Errorf("failed to get function metadata: %w", err)
}
switch resp.StatusCode() {
case http.StatusNotFound:
return nil, errors.Errorf("Function %s does not exist on the Supabase project.", utils.Aqua(slug))
case http.StatusOK:
break
default:
return nil, errors.Errorf("Failed to download Function %s on the Supabase project: %s", utils.Aqua(slug), string(resp.Body))
}
if resp.JSON200.EntrypointPath == nil {
resp.JSON200.EntrypointPath = &legacyEntrypointPath
}
if resp.JSON200.ImportMapPath == nil {
resp.JSON200.ImportMapPath = &legacyImportMapPath
}
return resp.JSON200, nil
}
func downloadFunction(ctx context.Context, projectRef, slug, extractScriptPath string) error {
fmt.Println("Downloading " + utils.Bold(slug))
denoPath, err := utils.GetDenoPath()
if err != nil {
return err
}
meta, err := getFunctionMetadata(ctx, projectRef, slug)
if err != nil {
return err
}
resp, err := utils.GetSupabase().V1GetAFunctionBodyWithResponse(ctx, projectRef, slug)
if err != nil {
return errors.Errorf("failed to get function body: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return errors.New("Unexpected error downloading Function: " + string(resp.Body))
}
resBuf := bytes.NewReader(resp.Body)
funcDir := filepath.Join(utils.FunctionsDir, slug)
args := []string{"run", "-A", extractScriptPath, funcDir, *meta.EntrypointPath}
cmd := exec.CommandContext(ctx, denoPath, args...)
var errBuf bytes.Buffer
cmd.Stdin = resBuf
cmd.Stdout = os.Stdout
cmd.Stderr = &errBuf
if err := cmd.Run(); err != nil {
return errors.Errorf("Error downloading function: %w\n%v", err, errBuf.String())
}
return nil
}
func Run(ctx context.Context, slug string, projectRef string, useLegacyBundle bool, fsys afero.Fs) error {
if useLegacyBundle {
return RunLegacy(ctx, slug, projectRef, fsys)
}
// 1. Sanity check
if err := flags.LoadConfig(fsys); err != nil {
return err
}
// 2. Download eszip to temp file
eszipPath, err := downloadOne(ctx, slug, projectRef, fsys)
if err != nil {
return err
}
defer func() {
if err := fsys.Remove(eszipPath); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}()
// Extract eszip to functions directory
err = extractOne(ctx, slug, eszipPath)
if err != nil {
utils.CmdSuggestion += suggestLegacyBundle(slug)
}
return err
}
func downloadOne(ctx context.Context, slug, projectRef string, fsys afero.Fs) (string, error) {
fmt.Println("Downloading " + utils.Bold(slug))
resp, err := utils.GetSupabase().V1GetAFunctionBody(ctx, projectRef, slug)
if err != nil {
return "", errors.Errorf("failed to get function body: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.Errorf("Error status %d: unexpected error downloading Function", resp.StatusCode)
}
return "", errors.Errorf("Error status %d: %s", resp.StatusCode, string(body))
}
// Create temp file to store downloaded eszip
eszipPath := filepath.Join(utils.TempDir, fmt.Sprintf("output_%s.eszip", slug))
if err := utils.MkdirIfNotExistFS(fsys, utils.TempDir); err != nil {
return "", err
}
if err := afero.WriteReader(fsys, eszipPath, resp.Body); err != nil {
return "", errors.Errorf("failed to download file: %w", err)
}
return eszipPath, nil
}
func extractOne(ctx context.Context, slug, eszipPath string) error {
hostFuncDirPath, err := filepath.Abs(filepath.Join(utils.FunctionsDir, slug))
if err != nil {
return errors.Errorf("failed to resolve absolute path: %w", err)
}
hostEszipPath, err := filepath.Abs(eszipPath)
if err != nil {
return errors.Errorf("failed to resolve eszip path: %w", err)
}
dockerEszipPath := path.Join(utils.DockerEszipDir, filepath.Base(hostEszipPath))
binds := []string{
// Reuse deno cache directory, ie. DENO_DIR, between container restarts
// https://denolib.gitbook.io/guide/advanced/deno_dir-code-fetch-and-cache
utils.EdgeRuntimeId + ":/root/.cache/deno:rw",
hostEszipPath + ":" + dockerEszipPath + ":ro",
hostFuncDirPath + ":" + utils.DockerDenoDir + ":rw",
}
return utils.DockerRunOnceWithConfig(
ctx,
container.Config{
Image: utils.Config.EdgeRuntime.Image,
Cmd: []string{"unbundle", "--eszip", dockerEszipPath, "--output", utils.DockerDenoDir},
},
container.HostConfig{
Binds: binds,
},
network.NetworkingConfig{},
"",
os.Stdout,
os.Stderr,
)
}
func suggestLegacyBundle(slug string) string {
return fmt.Sprintf("\nIf your function is deployed using CLI < 1.120.0, trying running %s instead.", utils.Aqua("supabase functions download --legacy-bundle "+slug))
}