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 }