• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 19237093902

10 Nov 2025 03:38PM UTC coverage: 55.419% (+0.7%) from 54.689%
19237093902

Pull #4381

github

web-flow
Merge d07722c3d into b5c28cafc
Pull Request #4381: feat: `functions download foo --use-api`

182 of 271 new or added lines in 2 files covered. (67.16%)

5 existing lines in 1 file now uncovered.

6622 of 11949 relevant lines covered (55.42%)

6.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.89
/internal/functions/download/download.go
1
package download
2

3
import (
4
        "bufio"
5
        "bytes"
6
        "context"
7
        "fmt"
8
        "io"
9
        "mime"
10
        "mime/multipart"
11
        "net/http"
12
        "net/url"
13
        "os"
14
        "os/exec"
15
        "path"
16
        "path/filepath"
17
        "strings"
18

19
        "github.com/andybalholm/brotli"
20
        "github.com/docker/docker/api/types/container"
21
        "github.com/docker/docker/api/types/network"
22
        "github.com/go-errors/errors"
23
        "github.com/spf13/afero"
24
        "github.com/spf13/viper"
25
        "github.com/supabase/cli/internal/utils"
26
        "github.com/supabase/cli/internal/utils/flags"
27
        "github.com/supabase/cli/pkg/api"
28
)
29

30
var (
31
        legacyEntrypointPath = "file:///src/index.ts"
32
        legacyImportMapPath  = "file:///src/import_map.json"
33
)
34

35
func RunLegacy(ctx context.Context, slug string, projectRef string, fsys afero.Fs) error {
5✔
36
        // 1. Sanity checks.
5✔
37
        {
10✔
38
                if err := utils.ValidateFunctionSlug(slug); err != nil {
6✔
39
                        return err
1✔
40
                }
1✔
41
        }
42
        if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil {
5✔
43
                return err
1✔
44
        }
1✔
45

46
        scriptDir, err := utils.CopyDenoScripts(ctx, fsys)
3✔
47
        if err != nil {
4✔
48
                return err
1✔
49
        }
1✔
50

51
        // 2. Download Function.
52
        if err := downloadFunction(ctx, projectRef, slug, scriptDir.ExtractPath); err != nil {
3✔
53
                return err
1✔
54
        }
1✔
55

56
        fmt.Println("Downloaded Function " + utils.Aqua(slug) + " from project " + utils.Aqua(projectRef) + ".")
1✔
57
        return nil
1✔
58
}
59

60
func getFunctionMetadata(ctx context.Context, projectRef, slug string) (*api.FunctionSlugResponse, error) {
13✔
61
        resp, err := utils.GetSupabase().V1GetAFunctionWithResponse(ctx, projectRef, slug)
13✔
62
        if err != nil {
14✔
63
                return nil, errors.Errorf("failed to get function metadata: %w", err)
1✔
64
        }
1✔
65

66
        switch resp.StatusCode() {
12✔
67
        case http.StatusNotFound:
1✔
68
                return nil, errors.Errorf("Function %s does not exist on the Supabase project.", utils.Aqua(slug))
1✔
69
        case http.StatusOK:
10✔
70
                break
10✔
71
        default:
1✔
72
                return nil, errors.Errorf("Failed to download Function %s on the Supabase project: %s", utils.Aqua(slug), string(resp.Body))
1✔
73
        }
74

75
        if resp.JSON200.EntrypointPath == nil {
15✔
76
                resp.JSON200.EntrypointPath = &legacyEntrypointPath
5✔
77
        }
5✔
78
        if resp.JSON200.ImportMapPath == nil {
20✔
79
                resp.JSON200.ImportMapPath = &legacyImportMapPath
10✔
80
        }
10✔
81
        return resp.JSON200, nil
10✔
82
}
83

84
func downloadFunction(ctx context.Context, projectRef, slug, extractScriptPath string) error {
5✔
85
        fmt.Println("Downloading " + utils.Bold(slug))
5✔
86
        denoPath, err := utils.GetDenoPath()
5✔
87
        if err != nil {
5✔
88
                return err
×
89
        }
×
90

91
        meta, err := getFunctionMetadata(ctx, projectRef, slug)
5✔
92
        if err != nil {
6✔
93
                return err
1✔
94
        }
1✔
95

96
        resp, err := utils.GetSupabase().V1GetAFunctionBodyWithResponse(ctx, projectRef, slug)
4✔
97
        if err != nil {
5✔
98
                return errors.Errorf("failed to get function body: %w", err)
1✔
99
        }
1✔
100
        if resp.StatusCode() != http.StatusOK {
4✔
101
                return errors.New("Unexpected error downloading Function: " + string(resp.Body))
1✔
102
        }
1✔
103

104
        resBuf := bytes.NewReader(resp.Body)
2✔
105
        funcDir := filepath.Join(utils.FunctionsDir, slug)
2✔
106
        args := []string{"run", "-A", extractScriptPath, funcDir, *meta.EntrypointPath}
2✔
107
        cmd := exec.CommandContext(ctx, denoPath, args...)
2✔
108
        var errBuf bytes.Buffer
2✔
109
        cmd.Stdin = resBuf
2✔
110
        cmd.Stdout = os.Stdout
2✔
111
        cmd.Stderr = &errBuf
2✔
112
        if err := cmd.Run(); err != nil {
3✔
113
                return errors.Errorf("Error downloading function: %w\n%v", err, errBuf.String())
1✔
114
        }
1✔
115
        return nil
1✔
116
}
117

118
func Run(ctx context.Context, slug, projectRef string, useLegacyBundle, useDocker bool, fsys afero.Fs) error {
11✔
119
        // Sanity check
11✔
120
        if err := flags.LoadConfig(fsys); err != nil {
11✔
NEW
121
                return err
×
NEW
122
        }
×
123

124
        if useLegacyBundle {
16✔
125
                return RunLegacy(ctx, slug, projectRef, fsys)
5✔
126
        }
5✔
127

128
        if useDocker {
8✔
129
                if utils.IsDockerRunning(ctx) {
3✔
130
                        // download eszip file for client-side unbundling with edge-runtime
1✔
131
                        return downloadWithDockerUnbundle(ctx, slug, projectRef, fsys)
1✔
132
                } else {
2✔
133
                        fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Docker is not running")
1✔
134
                }
1✔
135
        }
136

137
        // Use server-side unbundling with multipart/form-data
138
        return downloadWithServerSideUnbundle(ctx, slug, projectRef, fsys)
5✔
139
}
140

141
func downloadWithDockerUnbundle(ctx context.Context, slug string, projectRef string, fsys afero.Fs) error {
1✔
142
        eszipPath, err := downloadOne(ctx, slug, projectRef, fsys)
1✔
143
        if err != nil {
1✔
144
                return err
×
145
        }
×
146
        if !viper.GetBool("DEBUG") {
2✔
147
                defer func() {
2✔
148
                        if err := fsys.Remove(eszipPath); err != nil {
1✔
149
                                fmt.Fprintln(os.Stderr, err)
×
150
                        }
×
151
                }()
152
        }
153
        // Extract eszip to functions directory
154
        err = extractOne(ctx, slug, eszipPath)
1✔
155
        if err != nil {
1✔
156
                utils.CmdSuggestion += suggestLegacyBundle(slug)
×
157
        }
×
158
        return err
1✔
159
}
160

161
func downloadOne(ctx context.Context, slug, projectRef string, fsys afero.Fs) (string, error) {
1✔
162
        fmt.Println("Downloading " + utils.Bold(slug))
1✔
163
        resp, err := utils.GetSupabase().V1GetAFunctionBody(ctx, projectRef, slug)
1✔
164
        if err != nil {
1✔
165
                return "", errors.Errorf("failed to get function body: %w", err)
×
166
        }
×
167
        defer resp.Body.Close()
1✔
168
        if resp.StatusCode != http.StatusOK {
1✔
169
                body, err := io.ReadAll(resp.Body)
×
170
                if err != nil {
×
171
                        return "", errors.Errorf("Error status %d: unexpected error downloading Function", resp.StatusCode)
×
172
                }
×
173
                return "", errors.Errorf("Error status %d: %s", resp.StatusCode, string(body))
×
174
        }
175
        r := io.Reader(resp.Body)
1✔
176
        if strings.EqualFold(resp.Header.Get("Content-Encoding"), "br") {
1✔
177
                r = brotli.NewReader(resp.Body)
×
178
        }
×
179
        // Create temp file to store downloaded eszip
180
        eszipPath := filepath.Join(utils.TempDir, fmt.Sprintf("output_%s.eszip", slug))
1✔
181
        if err := utils.MkdirIfNotExistFS(fsys, utils.TempDir); err != nil {
1✔
182
                return "", err
×
183
        }
×
184
        if err := afero.WriteReader(fsys, eszipPath, r); err != nil {
1✔
185
                return "", errors.Errorf("failed to download file: %w", err)
×
186
        }
×
187
        return eszipPath, nil
1✔
188
}
189

190
func extractOne(ctx context.Context, slug, eszipPath string) error {
1✔
191
        hostFuncDirPath, err := filepath.Abs(filepath.Join(utils.FunctionsDir, slug))
1✔
192
        if err != nil {
1✔
193
                return errors.Errorf("failed to resolve absolute path: %w", err)
×
194
        }
×
195

196
        hostEszipPath, err := filepath.Abs(eszipPath)
1✔
197
        if err != nil {
1✔
198
                return errors.Errorf("failed to resolve eszip path: %w", err)
×
199
        }
×
200
        dockerEszipPath := path.Join(utils.DockerEszipDir, filepath.Base(hostEszipPath))
1✔
201

1✔
202
        binds := []string{
1✔
203
                // Reuse deno cache directory, ie. DENO_DIR, between container restarts
1✔
204
                // https://denolib.gitbook.io/guide/advanced/deno_dir-code-fetch-and-cache
1✔
205
                utils.EdgeRuntimeId + ":/root/.cache/deno:rw",
1✔
206
                hostEszipPath + ":" + dockerEszipPath + ":ro",
1✔
207
                hostFuncDirPath + ":" + utils.DockerDenoDir + ":rw",
1✔
208
        }
1✔
209

1✔
210
        return utils.DockerRunOnceWithConfig(
1✔
211
                ctx,
1✔
212
                container.Config{
1✔
213
                        Image: utils.Config.EdgeRuntime.Image,
1✔
214
                        Cmd:   []string{"unbundle", "--eszip", dockerEszipPath, "--output", utils.DockerDenoDir},
1✔
215
                },
1✔
216
                container.HostConfig{
1✔
217
                        Binds: binds,
1✔
218
                },
1✔
219
                network.NetworkingConfig{},
1✔
220
                "",
1✔
221
                os.Stdout,
1✔
222
                getErrorLogger(),
1✔
223
        )
1✔
224
}
225

226
func getErrorLogger() io.Writer {
1✔
227
        if utils.Config.EdgeRuntime.DenoVersion > 1 {
2✔
228
                return os.Stderr
1✔
229
        }
1✔
230
        // Additional error handling for deno v1
231
        r, w := io.Pipe()
×
232
        go func() {
×
233
                logs := bufio.NewScanner(r)
×
234
                for logs.Scan() {
×
235
                        line := logs.Text()
×
236
                        fmt.Fprintln(os.Stderr, line)
×
237
                        if strings.EqualFold(line, "invalid eszip v2") {
×
238
                                utils.CmdSuggestion = suggestDenoV2()
×
239
                        }
×
240
                }
241
                if err := logs.Err(); err != nil {
×
242
                        fmt.Fprintln(os.Stderr, err)
×
243
                }
×
244
        }()
245
        return w
×
246
}
247

248
func suggestDenoV2() string {
×
249
        return fmt.Sprintf(`Please use deno v2 in %s to download this Function:
×
250

×
251
[edge_runtime]
×
252
deno_version = 2
×
253
`, utils.Bold(utils.ConfigPath))
×
254
}
×
255

256
func suggestLegacyBundle(slug string) string {
×
257
        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))
×
258
}
×
259

260
// New server-side unbundle implementation that mirrors Studio's entrypoint-based
261
// base-dir + relative path behaviour.
262
func downloadWithServerSideUnbundle(ctx context.Context, slug, projectRef string, fsys afero.Fs) error {
5✔
263
        fmt.Fprintln(os.Stderr, "Downloading "+utils.Bold(slug))
5✔
264

5✔
265
        metadata, err := getFunctionMetadata(ctx, projectRef, slug)
5✔
266
        if err != nil {
5✔
NEW
267
                return errors.Errorf("failed to get function metadata: %w", err)
×
NEW
268
        }
×
269

270
        entrypointUrl, err := url.Parse(*metadata.EntrypointPath)
5✔
271
        if err != nil {
5✔
NEW
272
                return errors.Errorf("failed to parse entrypoint URL: %w", err)
×
NEW
273
        }
×
274

275
        // Request multipart/form-data response using RequestEditorFn
276
        resp, err := utils.GetSupabase().V1GetAFunctionBody(ctx, projectRef, slug, func(ctx context.Context, req *http.Request) error {
10✔
277
                req.Header.Set("Accept", "multipart/form-data")
5✔
278
                return nil
5✔
279
        })
5✔
280
        if err != nil {
5✔
NEW
281
                return errors.Errorf("failed to download function: %w", err)
×
NEW
282
        }
×
283
        defer resp.Body.Close()
5✔
284

5✔
285
        if resp.StatusCode != http.StatusOK {
5✔
NEW
286
                body, err := io.ReadAll(resp.Body)
×
NEW
287
                if err != nil {
×
NEW
288
                        return errors.Errorf("Error status %d: %w", resp.StatusCode, err)
×
NEW
289
                }
×
NEW
290
                return errors.Errorf("Error status %d: %s", resp.StatusCode, string(body))
×
291
        }
292

293
        // Parse the multipart response
294
        mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
5✔
295
        if err != nil {
5✔
NEW
296
                return errors.Errorf("failed to parse content type: %w", err)
×
NEW
297
        }
×
298

299
        if !strings.HasPrefix(mediaType, "multipart/") {
6✔
300
                return errors.Errorf("expected multipart response, got %s", mediaType)
1✔
301
        }
1✔
302

303
        // Root directory on disk: supabase/functions/<slug>
304
        funcDir := filepath.Join(utils.FunctionsDir, slug)
4✔
305
        if err := utils.MkdirIfNotExistFS(fsys, funcDir); err != nil {
4✔
NEW
306
                return err
×
NEW
307
        }
×
308

309
        bufferedParts, cleanupTemp, err := bufferMultipartParts(resp.Body, params["boundary"], slug, fsys)
4✔
310
        if cleanupTemp != nil {
8✔
311
                defer cleanupTemp()
4✔
312
        }
4✔
313
        if err != nil {
4✔
NEW
314
                return err
×
NEW
315
        }
×
316

317
        // Collect file paths (excluding empty ones) to infer the base directory.
318
        var filepaths []string
4✔
319
        for _, p := range bufferedParts {
11✔
320
                if p.path != "" {
14✔
321
                        filepaths = append(filepaths, p.path)
7✔
322
                }
7✔
323
        }
324

325
        // infer baseDir using a number of heuristics, in the simple case just Path.Dir(entrypoint)
326
        baseDir := getBaseDirFromEntrypoint(entrypointUrl, filepaths)
4✔
327

4✔
328
        // Place each file under funcDir using a path relative to baseDir,
4✔
329
        // mirroring Studio's getBasePath + relative() behavior.
4✔
330
        for _, p := range bufferedParts {
11✔
331
                if p.path == "" {
7✔
NEW
332
                        continue
×
333
                }
334

335
                relPath := getRelativePathFromBase(baseDir, p.path)
7✔
336
                filePath, err := joinWithinDir(funcDir, relPath)
7✔
337
                if err != nil {
8✔
338
                        return err
1✔
339
                }
1✔
340

341
                if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(filePath)); err != nil {
6✔
NEW
342
                        return err
×
NEW
343
                }
×
344

345
                if err := copyFileFromTemp(fsys, p.tempPath, filePath); err != nil {
6✔
NEW
346
                        return err
×
NEW
347
                }
×
348
        }
349

350
        fmt.Println("Downloaded Function " + utils.Aqua(slug) + " from project " + utils.Aqua(projectRef) + ".")
3✔
351
        return nil
3✔
352
}
353

354
type bufferedPart struct {
355
        path     string
356
        tempPath string
357
}
358

359
// write multipart parts to temp files and return their corresponding path pairs
360
func bufferMultipartParts(body io.Reader, boundary, slug string, fsys afero.Fs) ([]bufferedPart, func(), error) {
4✔
361
        if boundary == "" {
4✔
NEW
362
                return nil, nil, errors.New("multipart boundary missing")
×
NEW
363
        }
×
364
        if err := utils.MkdirIfNotExistFS(fsys, utils.TempDir); err != nil {
4✔
NEW
365
                return nil, nil, err
×
NEW
366
        }
×
367
        tempDir, err := afero.TempDir(fsys, utils.TempDir, fmt.Sprintf("functions-download-%s-", slug))
4✔
368
        if err != nil {
4✔
NEW
369
                return nil, nil, errors.Errorf("failed to create temp directory: %w", err)
×
NEW
370
        }
×
371
        afs := afero.Afero{Fs: fsys}
4✔
372
        cleanup := func() {
8✔
373
                if err := afs.RemoveAll(tempDir); err != nil {
4✔
NEW
374
                        fmt.Fprintln(utils.GetDebugLogger(), "failed to clean up temp dir:", err)
×
NEW
375
                }
×
376
        }
377

378
        mr := multipart.NewReader(body, boundary)
4✔
379
        var parts []bufferedPart
4✔
380
        for {
15✔
381
                part, err := mr.NextPart()
11✔
382
                if errors.Is(err, io.EOF) {
15✔
383
                        break
4✔
384
                }
385
                if err != nil {
7✔
NEW
386
                        cleanup()
×
NEW
387
                        return nil, nil, errors.Errorf("failed to read multipart: %w", err)
×
NEW
388
                }
×
389

390
                if err := func() error {
14✔
391
                        defer part.Close()
7✔
392
                        partPath, err := getPartPath(part)
7✔
393
                        if err != nil {
7✔
NEW
394
                                return err
×
NEW
395
                        }
×
396

397
                        if partPath == "" {
7✔
NEW
398
                                fmt.Fprintln(utils.GetDebugLogger(), "Skipping part without filename")
×
NEW
399
                                if _, err := io.Copy(io.Discard, part); err != nil {
×
NEW
400
                                        return errors.Errorf("failed to discard unnamed part: %w", err)
×
NEW
401
                                }
×
NEW
402
                                return nil
×
403
                        }
404

405
                        tmpFile, err := afero.TempFile(fsys, tempDir, "part-*")
7✔
406
                        if err != nil {
7✔
NEW
407
                                return errors.Errorf("failed to create temp file: %w", err)
×
NEW
408
                        }
×
409

410
                        if _, err := io.Copy(tmpFile, part); err != nil {
7✔
NEW
411
                                tmpFile.Close()
×
NEW
412
                                if rmErr := fsys.Remove(tmpFile.Name()); rmErr != nil {
×
NEW
413
                                        fmt.Fprintln(utils.GetDebugLogger(), "failed to remove temp file:", rmErr)
×
NEW
414
                                }
×
NEW
415
                                return errors.Errorf("failed to buffer part data: %w", err)
×
416
                        }
417
                        if err := tmpFile.Close(); err != nil {
7✔
NEW
418
                                if rmErr := fsys.Remove(tmpFile.Name()); rmErr != nil {
×
NEW
419
                                        fmt.Fprintln(utils.GetDebugLogger(), "failed to remove temp file:", rmErr)
×
NEW
420
                                }
×
NEW
421
                                return errors.Errorf("failed to close temp file: %w", err)
×
422
                        }
423

424
                        parts = append(parts, bufferedPart{path: partPath, tempPath: tmpFile.Name()})
7✔
425
                        return nil
7✔
NEW
426
                }(); err != nil {
×
NEW
427
                        cleanup()
×
NEW
428
                        return nil, nil, err
×
NEW
429
                }
×
430
        }
431

432
        return parts, cleanup, nil
4✔
433
}
434

435
// getPartPath extracts the filename for a multipart part, allowing for
436
// relative paths via the custom Supabase-Path header.
437
func getPartPath(part *multipart.Part) (string, error) {
13✔
438
        // dedicated header to specify relative path, not expected to be used
13✔
439
        if relPath := part.Header.Get("Supabase-Path"); relPath != "" {
15✔
440
                return relPath, nil
2✔
441
        }
2✔
442

443
        // part.FileName() does not allow us to handle relative paths, so we parse Content-Disposition manually
444
        cd := part.Header.Get("Content-Disposition")
11✔
445
        if cd == "" {
11✔
NEW
446
                return "", nil
×
NEW
447
        }
×
448

449
        _, params, err := mime.ParseMediaType(cd)
11✔
450
        if err != nil {
12✔
451
                return "", errors.Errorf("failed to parse content disposition: %w", err)
1✔
452
        }
1✔
453

454
        if filename := params["filename"]; filename != "" {
19✔
455
                return filename, nil
9✔
456
        }
9✔
457
        return "", nil
1✔
458
}
459

460
// joinWithinDir safely joins base and rel ensuring the result stays within base directory
461
func joinWithinDir(base, rel string) (string, error) {
13✔
462
        cleanBase := filepath.Clean(base)
13✔
463
        cleanRel := filepath.Clean(rel)
13✔
464
        // Treat absolute inputs as relative by stripping leading separators ("/foo" -> "foo").
13✔
465
        if filepath.IsAbs(cleanRel) {
14✔
466
                cleanRel = strings.TrimLeft(cleanRel, "/\\")
1✔
467
        }
1✔
468

469
        // Reject direct attempts to escape (e.g. "../secret.env" or "..\secret.env").
470
        if cleanRel == ".." || strings.HasPrefix(cleanRel, "..") {
17✔
471
                return "", errors.Errorf("invalid file path outside function directory: %s", rel)
4✔
472
        }
4✔
473

474
        // Join the sanitized components and normalize the result to remove any "." segments.
475
        joined := filepath.Join(cleanBase, cleanRel)
9✔
476
        cleanJoined := filepath.Clean(joined)
9✔
477

9✔
478
        // Compute the final relative path. If it still points outside the base (starts with ".."),
9✔
479
        // refuse to write the file.
9✔
480
        relPath, err := filepath.Rel(cleanBase, cleanJoined)
9✔
481
        if err != nil {
9✔
NEW
482
                return "", errors.Errorf("failed to resolve relative path: %w", err)
×
NEW
483
        }
×
484
        if relPath == ".." || strings.HasPrefix(relPath, "..") {
9✔
NEW
485
                return "", errors.Errorf("refusing to write outside function directory: %s", rel)
×
NEW
486
        }
×
487
        return joined, nil
9✔
488
}
489

490
// getBaseDirFromEntrypoint tries to infer the "base" directory for function
491
// files from the entrypoint URL and the list of filenames, similar to Studio's
492
// getBasePath logic.
493
func getBaseDirFromEntrypoint(entrypointUrl *url.URL, filenames []string) string {
8✔
494
        if entrypointUrl.Path == "" {
8✔
NEW
495
                return "/"
×
NEW
496
        }
×
497

498
        entryPath := filepath.ToSlash(entrypointUrl.Path)
8✔
499

8✔
500
        // First, prefer relative filenames (no leading slash) when matching the entrypoint.
8✔
501
        var baseDir string
8✔
502
        for _, filename := range filenames {
16✔
503
                if filename == "" {
8✔
NEW
504
                        continue
×
505
                }
506
                clean := filepath.ToSlash(filename)
8✔
507
                if strings.HasPrefix(clean, "/") {
12✔
508
                        // Skip absolute paths like /tmp/...
4✔
509
                        continue
4✔
510
                }
511
                if strings.HasSuffix(entryPath, clean) {
8✔
512
                        baseDir = filepath.Dir(clean)
4✔
513
                        break
4✔
514
                }
515
        }
516

517
        // If nothing matched among relative paths, fall back to any filename.
518
        if baseDir == "" {
12✔
519
                for _, filename := range filenames {
7✔
520
                        if filename == "" {
3✔
NEW
521
                                continue
×
522
                        }
523
                        clean := filepath.ToSlash(filename)
3✔
524
                        // entrypoint has the same suffix as the sanitized filename
3✔
525
                        matchRelative := strings.HasSuffix(entryPath, clean)
3✔
526

3✔
527
                        // prevents long absolute paths being used as subdirectories
3✔
528
                        matchAbsolute := strings.HasSuffix(clean, entryPath)
3✔
529
                        if matchRelative || matchAbsolute {
5✔
530
                                baseDir = filepath.Dir(clean)
2✔
531
                                break
2✔
532
                        }
533
                }
534
        }
535

536
        if baseDir != "" {
14✔
537
                return baseDir
6✔
538
        }
6✔
539

540
        // Final fallback: derive from the entrypoint URL path itself.
541
        baseDir = filepath.Dir(entrypointUrl.Path)
2✔
542
        if baseDir != "" && baseDir != "." {
4✔
543
                return baseDir
2✔
544
        }
2✔
NEW
545
        return "/"
×
546
}
547

548
// getRelativePathFromBase mirrors the Studio behaviour of making file paths
549
// relative to the "base" directory inferred from the entrypoint.
550
func getRelativePathFromBase(baseDir, filename string) string {
13✔
551
        if filename == "" {
13✔
NEW
552
                return ""
×
NEW
553
        }
×
554

555
        cleanBase := filepath.ToSlash(filepath.Clean(baseDir))
13✔
556
        cleanFile := filepath.ToSlash(filepath.Clean(filename))
13✔
557

13✔
558
        // If we don't have a meaningful base, just normalize to a relative path.
13✔
559
        if cleanBase == "" || cleanBase == "/" || cleanBase == "." {
15✔
560
                return strings.TrimLeft(cleanFile, "/")
2✔
561
        }
2✔
562

563
        // Try a straightforward relative path first (e.g. source/index.ts -> index.ts).
564
        if rel, err := filepath.Rel(cleanBase, cleanFile); err == nil && rel != "." && !strings.HasPrefix(rel, "..") {
19✔
565
                return filepath.ToSlash(rel)
8✔
566
        }
8✔
567

568
        // If the file path contains "/<baseDir>/" somewhere (e.g. /tmp/.../source/index.ts),
569
        // strip everything up to and including that segment so we get a stable relative path
570
        // like "index.ts" or "dir/file.ts".
571
        segment := "/" + cleanBase + "/"
3✔
572
        if idx := strings.Index(cleanFile, segment); idx >= 0 {
4✔
573
                return cleanFile[idx+len(segment):]
1✔
574
        }
1✔
575

576
        // Last resort: return a normalized, slash-stripped path.
577
        return strings.TrimLeft(cleanFile, "/")
2✔
578
}
579

580
func copyFileFromTemp(fsys afero.Fs, src, dst string) error {
6✔
581
        tempFile, err := fsys.Open(src)
6✔
582
        if err != nil {
6✔
NEW
583
                return errors.Errorf("failed to open temp file: %w", err)
×
NEW
584
        }
×
585
        defer tempFile.Close()
6✔
586

6✔
587
        destFile, err := fsys.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
6✔
588
        if err != nil {
6✔
NEW
589
                return errors.Errorf("failed to create file: %w", err)
×
NEW
590
        }
×
591
        defer destFile.Close()
6✔
592

6✔
593
        if _, err := io.Copy(destFile, tempFile); err != nil {
6✔
NEW
594
                return errors.Errorf("failed to write file: %w", err)
×
NEW
595
        }
×
596
        return nil
6✔
597
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc