Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cmd/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/spf13/cobra"
"github.com/supabase/cli/internal/functions/delete"
"github.com/supabase/cli/internal/functions/deploy"
"github.com/supabase/cli/internal/functions/download"
new_ "github.com/supabase/cli/internal/functions/new"
"github.com/supabase/cli/internal/functions/serve"
"github.com/supabase/cli/internal/login"
Expand Down Expand Up @@ -42,6 +43,21 @@ var (
},
}

functionsDownloadCmd = &cobra.Command{
Use: "download <Function name>",
Short: "Download a Function from Supabase",
Long: "Download source code of a Function from the linked Supabase project.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
fsys := afero.NewOsFs()
if err := PromptLogin(fsys); err != nil {
return err
}
ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
return download.Run(ctx, args[0], projectRef, fsys)
},
}

noVerifyJWT = new(bool)
useLegacyBundle bool
importMapPath string
Expand Down Expand Up @@ -104,10 +120,12 @@ func init() {
functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
functionsServeCmd.Flags().StringVar(&envFilePath, "env-file", "", "Path to an env file to be populated to the Function environment.")
functionsServeCmd.Flags().StringVar(&importMapPath, "import-map", "", "Path to import map file.")
functionsDownloadCmd.Flags().StringVar(&projectRef, "project-ref", "", "Project ref of the Supabase project.")
functionsCmd.AddCommand(functionsDeleteCmd)
functionsCmd.AddCommand(functionsDeployCmd)
functionsCmd.AddCommand(functionsNewCmd)
functionsCmd.AddCommand(functionsServeCmd)
functionsCmd.AddCommand(functionsDownloadCmd)
rootCmd.AddCommand(functionsCmd)
}

Expand Down
5 changes: 3 additions & 2 deletions internal/functions/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const eszipContentType = "application/vnd.denoland.eszip"
func Run(ctx context.Context, slug string, projectRefArg string, noVerifyJWT *bool, useLegacyBundle bool, importMapPath string, fsys afero.Fs) error {
// 1. Sanity checks.
projectRef := projectRefArg
var buildScriptPath string
var scriptDir *utils.DenoScriptDir
{
if len(projectRefArg) == 0 {
ref, err := utils.LoadProjectRef(fsys)
Expand Down Expand Up @@ -68,7 +68,7 @@ func Run(ctx context.Context, slug string, projectRefArg string, noVerifyJWT *bo
}

var err error
buildScriptPath, err = utils.CopyEszipScripts(ctx, fsys)
scriptDir, err = utils.CopyDenoScripts(ctx, fsys)
if err != nil {
return err
}
Expand All @@ -94,6 +94,7 @@ func Run(ctx context.Context, slug string, projectRefArg string, noVerifyJWT *bo
}
}

buildScriptPath := scriptDir.BuildPath
args := []string{"run", "-A", buildScriptPath, filepath.Join(functionPath, "index.ts"), importMapPath}
if useLegacyBundle {
args = []string{"bundle", "--no-check=remote", "--quiet", filepath.Join(functionPath, "index.ts")}
Expand Down
82 changes: 82 additions & 0 deletions internal/functions/download/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package download

import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"

"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
)

func Run(ctx context.Context, slug string, projectRefArg string, fsys afero.Fs) error {
// 1. Sanity checks.
projectRef := projectRefArg
var scriptDir *utils.DenoScriptDir
{
if len(projectRefArg) == 0 {
ref, err := utils.LoadProjectRef(fsys)
if err != nil {
return err
}
projectRef = ref
} else if !utils.ProjectRefPattern.MatchString(projectRefArg) {
return errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.")
}
if err := utils.ValidateFunctionSlug(slug); err != nil {
return err
}
}
if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil {
return err
}

var err error
scriptDir, err = utils.CopyDenoScripts(ctx, fsys)
if err != nil {
return err
}

// 2. Download Function.
{
fmt.Println("Downloading " + utils.Bold(slug))
denoPath, err := utils.GetDenoPath()
if err != nil {
return err
}

resp, err := utils.GetSupabase().GetFunctionBodyWithResponse(ctx, projectRef, slug)
if err != nil {
return err
}

switch resp.StatusCode() {
case http.StatusNotFound: // Function doesn't exist
return errors.New("Function " + utils.Aqua(slug) + " does not exist on the Supabase project.")
case http.StatusOK: // Function exists
resBuf := bytes.NewReader(resp.Body)

extractScriptPath := scriptDir.ExtractPath
funcDir := filepath.Join(utils.FunctionsDir, slug)
var errBuf bytes.Buffer
args := []string{"run", "-A", extractScriptPath, funcDir}
cmd := exec.CommandContext(ctx, denoPath, args...)
cmd.Stdin = resBuf
cmd.Stdout = os.Stdout
cmd.Stderr = &errBuf
if err := cmd.Run(); err != nil {
return fmt.Errorf("Error downloading function: %w\n%v", err, errBuf.String())
}
default:
return errors.New("Unexpected error downloading Function: " + string(resp.Body))
}
}

fmt.Println("Downloaded Function " + utils.Aqua(slug) + " from project " + utils.Aqua(projectRef) + ".")
return nil
}
File renamed without changes.
45 changes: 45 additions & 0 deletions internal/utils/denos/extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as path from "https://deno.land/std@0.127.0/path/mod.ts";
import { readAll } from "https://deno.land/std@0.162.0/streams/conversion.ts";
import { decompress } from "https://deno.land/x/brotli@0.1.7/mod.ts";

import { Parser } from "https://deno.land/x/eszip@v0.30.0/mod.ts";

function url2path(url: string) {
const srcPath = new URL(url).pathname.replace("/src", "");
return path.join(...srcPath.split("/").filter(Boolean));
}

async function write(p: string, content: string) {
await Deno.mkdir(path.dirname(p), { recursive: true });
await Deno.writeTextFile(p, content);
}

async function loadEszip(bytes: Uint8Array) {
const parser = await Parser.createInstance();
const specifiers = await parser.parseBytes(bytes);
await parser.load();
return { parser, specifiers };
}

async function extractEszip(dest: string, parser, specifiers) {
const imports = {};

for (const specifier of specifiers) {
// skip remote dependencies
if (!specifier.startsWith("file:")) {
continue;
}
const module = await parser.getModuleSource(specifier);
await write(path.join(dest, url2path(specifier)), module);
}
}

async function extractSource(dest: string) {
const buf = await readAll(Deno.stdin);
// response is compressed with Brotli
const decompressed = decompress(buf);
const { parser, specifiers } = await loadEszip(decompressed);
await extractEszip(dest, parser, specifiers);
}

extractSource(Deno.args[0]);
66 changes: 36 additions & 30 deletions internal/utils/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ var (
//go:embed templates/globals.sql
GlobalsSql string

//go:embed eszip/*
eszipEmbedDir embed.FS
//go:embed denos/*
denoEmbedDir embed.FS

AccessTokenPattern = regexp.MustCompile(`^sbp_[a-f0-9]{40}$`)
ProjectRefPattern = regexp.MustCompile(`^[a-z]{20}$`)
Expand Down Expand Up @@ -408,52 +408,42 @@ func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error {
return nil
}

func isBuildScriptModified(fsys afero.Fs, buildScriptPath string) (bool, error) {
bs, err := afero.ReadFile(fsys, buildScriptPath)
func isScriptModified(fsys afero.Fs, destPath string, src []byte) (bool, error) {
dest, err := afero.ReadFile(fsys, destPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return true, nil
}
return false, err
}

es, err := fs.ReadFile(eszipEmbedDir, "eszip/build.ts")
if err != nil {
return false, err
}
// compare the md5 checksum of src bytes with user's copy.
// if the checksums doesn't match, script is modified.
return md5.Sum(dest) != md5.Sum(src), nil
}

// compare the md5 checksum of current build script with user's copy.
// if the checksums doesn't match, build script is modified.
return md5.Sum(bs) != md5.Sum(es), nil
type DenoScriptDir struct {
ExtractPath string
BuildPath string
}

// Copy ESZIP scripts needed for function deploy, returning the build script path or an error.
func CopyEszipScripts(ctx context.Context, fsys afero.Fs) (string, error) {
// Copy Deno scripts needed for function deploy and downloads, returning a DenoScriptDir struct or an error.
func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error) {
denoPath, err := GetDenoPath()
if err != nil {
return "", err
return nil, err
}

denoDirPath := filepath.Dir(denoPath)
scriptDirPath := filepath.Join(denoDirPath, "eszip")
buildScriptPath := filepath.Join(scriptDirPath, "build.ts")
scriptDirPath := filepath.Join(denoDirPath, "denos")

// make the script directory if not exist
if err := MkdirIfNotExistFS(fsys, scriptDirPath); err != nil {
return "", err
}

// check if the build script should be copied
modified, err := isBuildScriptModified(fsys, buildScriptPath)
if err != nil {
return "", err
}
if !modified {
return buildScriptPath, nil
return nil, err
}

// copy embed files to script directory
err = fs.WalkDir(eszipEmbedDir, "eszip", func(path string, d fs.DirEntry, err error) error {
err = fs.WalkDir(denoEmbedDir, "denos", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
Expand All @@ -463,10 +453,21 @@ func CopyEszipScripts(ctx context.Context, fsys afero.Fs) (string, error) {
return nil
}

contents, err := fs.ReadFile(eszipEmbedDir, path)
destPath := filepath.Join(denoDirPath, path)

contents, err := fs.ReadFile(denoEmbedDir, path)
if err != nil {
return err
}

// check if the script should be copied
modified, err := isScriptModified(fsys, destPath, contents)
if err != nil {
return err
}
if !modified {
return nil
}

if err := afero.WriteFile(fsys, filepath.Join(denoDirPath, path), contents, 0666); err != nil {
return err
Expand All @@ -476,10 +477,15 @@ func CopyEszipScripts(ctx context.Context, fsys afero.Fs) (string, error) {
})

if err != nil {
return "", err
return nil, err
}

sd := DenoScriptDir{
ExtractPath: filepath.Join(scriptDirPath, "extract.ts"),
BuildPath: filepath.Join(scriptDirPath, "build.ts"),
}

return filepath.Join(buildScriptPath), nil
return &sd, nil
}

func LoadAccessToken() (string, error) {
Expand Down