返回 DeepSeek-Reasonix
browser_upload_files.go
根目录 / desktop / browser_upload_files.go
1 package main
2
3 import (
4 "errors"
5 "fmt"
6 "io"
7 "os"
8 "path/filepath"
9 "strings"
10 )
11
12 // Uploads use a private copy of an owned file. OpenRoot keeps a concurrent
13 // symlink replacement from escaping the authorized workspace/scratch root.
14 // Remote broker uploads are first downloaded into this executor's scratch
15 // root, so they follow the same policy without trusting remote absolute paths.
16 func (e *hostBrowserExecutor) prepareUploadFiles(files []string) ([]string, func(), error) {
17 if len(files) == 0 {
18 return nil, nil, fmt.Errorf("browser upload: files are required")
19 }
20 scratch, err := e.captureDir()
21 if err != nil {
22 return nil, nil, err
23 }
24 roots := []string{scratch}
25 e.app.mu.RLock()
26 if tab := e.app.tabs[e.tabID]; tab != nil && tab.WorkspaceRoot != "" {
27 roots = append(roots, tab.WorkspaceRoot)
28 }
29 e.app.mu.RUnlock()
30 dir, err := os.MkdirTemp(scratch, "upload-")
31 if err != nil {
32 return nil, nil, err
33 }
34 cleanup := func() { _ = os.RemoveAll(dir) }
35 result := make([]string, 0, len(files))
36 for _, file := range files {
37 staged, err := stageOwnedBrowserFile(roots, dir, file)
38 if err != nil {
39 cleanup()
40 return nil, nil, err
41 }
42 result = append(result, staged)
43 }
44 return result, cleanup, nil
45 }
46
47 func stageOwnedBrowserFile(roots []string, directory, file string) (string, error) {
48 if !filepath.IsAbs(file) {
49 return "", fmt.Errorf("browser upload: file path must be absolute")
50 }
51 name := filepath.Base(file)
52 if !filepath.IsLocal(name) || name == "." || strings.ContainsAny(name, "/\\\x00") {
53 return "", fmt.Errorf("browser upload: file name must be a single local path component")
54 }
55 resolved, err := filepath.EvalSymlinks(file)
56 if err != nil {
57 return "", fmt.Errorf("browser upload: %w", err)
58 }
59 for _, candidate := range roots {
60 rootPath, err := filepath.EvalSymlinks(candidate)
61 if err != nil {
62 continue
63 }
64 rel, err := filepath.Rel(rootPath, resolved)
65 if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) {
66 continue
67 }
68 root, err := os.OpenRoot(rootPath)
69 if err != nil {
70 return "", err
71 }
72 input, err := root.Open(rel)
73 _ = root.Close()
74 if err != nil {
75 return "", err
76 }
77 defer input.Close()
78 info, err := input.Stat()
79 if err != nil {
80 return "", err
81 }
82 if !info.Mode().IsRegular() || info.Size() > browserRelayMaxBytes {
83 return "", fmt.Errorf("browser upload: file must be regular and at most %d bytes", browserRelayMaxBytes)
84 }
85 dir, err := os.MkdirTemp(directory, "file-")
86 if err != nil {
87 return "", err
88 }
89 outputRoot, err := os.OpenRoot(dir)
90 if err != nil {
91 return "", err
92 }
93 defer outputRoot.Close()
94 out, err := outputRoot.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
95 if err != nil {
96 return "", err
97 }
98 n, copyErr := io.Copy(out, io.LimitReader(input, browserRelayMaxBytes+1))
99 closeErr := out.Close()
100 if n > browserRelayMaxBytes {
101 copyErr = errors.Join(copyErr, fmt.Errorf("file exceeds %d bytes", browserRelayMaxBytes))
102 }
103 if copyErr != nil || closeErr != nil {
104 _ = outputRoot.Remove(name)
105 return "", fmt.Errorf("browser upload: staging failed: %w", errors.Join(copyErr, closeErr))
106 }
107 return filepath.Join(dir, name), nil
108 }
109 return "", fmt.Errorf("browser upload: file is outside this task's workspace and scratch directory")
110 }
111
111 lines GO