github.com/supabase/cli@v1.168.1/internal/functions/download/download.go (about)

     1  package download
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"os/exec"
    11  	"path"
    12  	"path/filepath"
    13  
    14  	"github.com/docker/docker/api/types/container"
    15  	"github.com/docker/docker/api/types/network"
    16  	"github.com/go-errors/errors"
    17  	"github.com/spf13/afero"
    18  	"github.com/supabase/cli/internal/db/start"
    19  	"github.com/supabase/cli/internal/utils"
    20  	"github.com/supabase/cli/pkg/api"
    21  )
    22  
    23  var (
    24  	legacyEntrypointPath = "file:///src/index.ts"
    25  	legacyImportMapPath  = "file:///src/import_map.json"
    26  )
    27  
    28  func RunLegacy(ctx context.Context, slug string, projectRef string, fsys afero.Fs) error {
    29  	// 1. Sanity checks.
    30  	{
    31  		if err := utils.ValidateFunctionSlug(slug); err != nil {
    32  			return err
    33  		}
    34  	}
    35  	if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil {
    36  		return err
    37  	}
    38  
    39  	scriptDir, err := utils.CopyDenoScripts(ctx, fsys)
    40  	if err != nil {
    41  		return err
    42  	}
    43  
    44  	// 2. Download Function.
    45  	if err := downloadFunction(ctx, projectRef, slug, scriptDir.ExtractPath); err != nil {
    46  		return err
    47  	}
    48  
    49  	fmt.Println("Downloaded Function " + utils.Aqua(slug) + " from project " + utils.Aqua(projectRef) + ".")
    50  	return nil
    51  }
    52  
    53  func getFunctionMetadata(ctx context.Context, projectRef, slug string) (*api.FunctionSlugResponse, error) {
    54  	resp, err := utils.GetSupabase().GetFunctionWithResponse(ctx, projectRef, slug)
    55  	if err != nil {
    56  		return nil, errors.Errorf("failed to get function metadata: %w", err)
    57  	}
    58  
    59  	switch resp.StatusCode() {
    60  	case http.StatusNotFound:
    61  		return nil, errors.Errorf("Function %s does not exist on the Supabase project.", utils.Aqua(slug))
    62  	case http.StatusOK:
    63  		break
    64  	default:
    65  		return nil, errors.Errorf("Failed to download Function %s on the Supabase project: %s", utils.Aqua(slug), string(resp.Body))
    66  	}
    67  
    68  	if resp.JSON200.EntrypointPath == nil {
    69  		resp.JSON200.EntrypointPath = &legacyEntrypointPath
    70  	}
    71  	if resp.JSON200.ImportMapPath == nil {
    72  		resp.JSON200.ImportMapPath = &legacyImportMapPath
    73  	}
    74  	return resp.JSON200, nil
    75  }
    76  
    77  func downloadFunction(ctx context.Context, projectRef, slug, extractScriptPath string) error {
    78  	fmt.Println("Downloading " + utils.Bold(slug))
    79  	denoPath, err := utils.GetDenoPath()
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	meta, err := getFunctionMetadata(ctx, projectRef, slug)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	resp, err := utils.GetSupabase().GetFunctionBodyWithResponse(ctx, projectRef, slug)
    90  	if err != nil {
    91  		return errors.Errorf("failed to get function body: %w", err)
    92  	}
    93  	if resp.StatusCode() != http.StatusOK {
    94  		return errors.New("Unexpected error downloading Function: " + string(resp.Body))
    95  	}
    96  
    97  	resBuf := bytes.NewReader(resp.Body)
    98  	funcDir := filepath.Join(utils.FunctionsDir, slug)
    99  	args := []string{"run", "-A", extractScriptPath, funcDir, *meta.EntrypointPath}
   100  	cmd := exec.CommandContext(ctx, denoPath, args...)
   101  	var errBuf bytes.Buffer
   102  	cmd.Stdin = resBuf
   103  	cmd.Stdout = os.Stdout
   104  	cmd.Stderr = &errBuf
   105  	if err := cmd.Run(); err != nil {
   106  		return errors.Errorf("Error downloading function: %w\n%v", err, errBuf.String())
   107  	}
   108  	return nil
   109  }
   110  
   111  const dockerEszipDir = "/root/eszips"
   112  
   113  func Run(ctx context.Context, slug string, projectRef string, useLegacyBundle bool, fsys afero.Fs) error {
   114  	if useLegacyBundle {
   115  		return RunLegacy(ctx, slug, projectRef, fsys)
   116  	}
   117  	// 1. Sanity check
   118  	if err := utils.LoadConfigFS(fsys); err != nil {
   119  		return err
   120  	}
   121  	// 2. Download eszip to temp file
   122  	eszipPath, err := downloadOne(ctx, slug, projectRef, fsys)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	defer func() {
   127  		if err := fsys.Remove(eszipPath); err != nil {
   128  			fmt.Fprintln(os.Stderr, err)
   129  		}
   130  	}()
   131  	// Extract eszip to functions directory
   132  	err = extractOne(ctx, slug, eszipPath)
   133  	if err != nil {
   134  		utils.CmdSuggestion += suggestLegacyBundle(slug)
   135  	}
   136  	return err
   137  }
   138  
   139  func downloadOne(ctx context.Context, slug, projectRef string, fsys afero.Fs) (string, error) {
   140  	fmt.Println("Downloading " + utils.Bold(slug))
   141  	resp, err := utils.GetSupabase().GetFunctionBody(ctx, projectRef, slug)
   142  	if err != nil {
   143  		return "", errors.Errorf("failed to get function body: %w", err)
   144  	}
   145  	defer resp.Body.Close()
   146  	if resp.StatusCode != http.StatusOK {
   147  		body, err := io.ReadAll(resp.Body)
   148  		if err != nil {
   149  			return "", errors.Errorf("Error status %d: unexpected error downloading Function", resp.StatusCode)
   150  		}
   151  		return "", errors.Errorf("Error status %d: %s", resp.StatusCode, string(body))
   152  	}
   153  	// Create temp file to store downloaded eszip
   154  	eszipPath := filepath.Join(utils.TempDir, fmt.Sprintf("output_%s.eszip", slug))
   155  	if err := utils.MkdirIfNotExistFS(fsys, utils.TempDir); err != nil {
   156  		return "", err
   157  	}
   158  	if err := afero.WriteReader(fsys, eszipPath, resp.Body); err != nil {
   159  		return "", errors.Errorf("failed to download file: %w", err)
   160  	}
   161  	return eszipPath, nil
   162  }
   163  
   164  func extractOne(ctx context.Context, slug, eszipPath string) error {
   165  	hostFuncDirPath, err := filepath.Abs(filepath.Join(utils.FunctionsDir, slug))
   166  	if err != nil {
   167  		return errors.Errorf("failed to resolve absolute path: %w", err)
   168  	}
   169  
   170  	hostEszipPath, err := filepath.Abs(eszipPath)
   171  	if err != nil {
   172  		return errors.Errorf("failed to resolve eszip path: %w", err)
   173  	}
   174  	dockerEszipPath := path.Join(dockerEszipDir, filepath.Base(hostEszipPath))
   175  
   176  	binds := []string{
   177  		// Reuse deno cache directory, ie. DENO_DIR, between container restarts
   178  		// https://denolib.gitbook.io/guide/advanced/deno_dir-code-fetch-and-cache
   179  		utils.EdgeRuntimeId + ":/root/.cache/deno:rw,z",
   180  		hostEszipPath + ":" + dockerEszipPath + ":ro,z",
   181  		hostFuncDirPath + ":" + utils.DockerDenoDir + ":rw,z",
   182  	}
   183  
   184  	return utils.DockerRunOnceWithConfig(
   185  		ctx,
   186  		container.Config{
   187  			Image: utils.EdgeRuntimeImage,
   188  			Cmd:   []string{"unbundle", "--eszip", dockerEszipPath, "--output", utils.DockerDenoDir},
   189  		},
   190  		start.WithSyslogConfig(container.HostConfig{
   191  			Binds:      binds,
   192  			ExtraHosts: []string{"host.docker.internal:host-gateway"},
   193  		}),
   194  		network.NetworkingConfig{},
   195  		"",
   196  		os.Stdout,
   197  		os.Stderr,
   198  	)
   199  }
   200  
   201  func suggestLegacyBundle(slug string) string {
   202  	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))
   203  }