-
Notifications
You must be signed in to change notification settings - Fork 902
feat: apps support multi dev modes #1175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,129 @@ | ||||||||||||||||||||||||||||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||||||||||||||||||||||||||||
| // SPDX-License-Identifier: MIT | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| package apps | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||
| "net/url" | ||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| "github.com/larksuite/cli/internal/core" | ||||||||||||||||||||||||||||
| "github.com/larksuite/cli/internal/validate" | ||||||||||||||||||||||||||||
| "github.com/larksuite/cli/internal/vfs" | ||||||||||||||||||||||||||||
|
Check failure on line 15 in shortcuts/apps/storage.go
|
||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // storageRoot is the per-domain local-storage directory name under the config dir. | ||||||||||||||||||||||||||||
| const storageRoot = "spark" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // checkSeg validates a value used as a single path segment (appID or key). | ||||||||||||||||||||||||||||
| // It rejects empty, "..", "." , URL metacharacters, control and dangerous | ||||||||||||||||||||||||||||
| // Unicode via validate.ResourceName — defense-in-depth alongside the | ||||||||||||||||||||||||||||
| // EncodePathSegment escaping applied when building the path, so neither value | ||||||||||||||||||||||||||||
| // can traverse out of the storage directory. | ||||||||||||||||||||||||||||
| func checkSeg(name, what string) error { | ||||||||||||||||||||||||||||
| if err := validate.ResourceName(name, what); err != nil { | ||||||||||||||||||||||||||||
| return fmt.Errorf("apps storage: %w", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if name == "." { | ||||||||||||||||||||||||||||
| return fmt.Errorf("apps storage: %s must not be \".\"", what) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // appDir returns the storage directory for one app: ~/.lark-cli/spark/<esc(appID)>/ | ||||||||||||||||||||||||||||
| // (workspace-aware). | ||||||||||||||||||||||||||||
| func appDir(appID string) string { | ||||||||||||||||||||||||||||
| return filepath.Join(core.GetConfigDir(), storageRoot, validate.EncodePathSegment(appID)) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // appKeyPath returns the file path for one (appID, key). | ||||||||||||||||||||||||||||
| func appKeyPath(appID, key string) string { | ||||||||||||||||||||||||||||
| return filepath.Join(appDir(appID), validate.EncodePathSegment(key)) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Read returns the bytes stored under (appID, key). A missing file returns | ||||||||||||||||||||||||||||
| // (nil, nil). Content is opaque — callers own the format. Note: an empty stored | ||||||||||||||||||||||||||||
| // value is indistinguishable from a missing key (both yield nil), so this store | ||||||||||||||||||||||||||||
| // is unsuitable as an existence flag. | ||||||||||||||||||||||||||||
| func Read(appID, key string) ([]byte, error) { | ||||||||||||||||||||||||||||
| if err := checkSeg(appID, "appID"); err != nil { | ||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if err := checkSeg(key, "key"); err != nil { | ||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| data, err := vfs.ReadFile(appKeyPath(appID, key)) | ||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||
| if errors.Is(err, os.ErrNotExist) { | ||||||||||||||||||||||||||||
| return nil, nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return nil, fmt.Errorf("apps storage: read: %w", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return data, nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Write atomically stores data under (appID, key): file 0600, dir 0700. It is a | ||||||||||||||||||||||||||||
| // create-or-replace upsert for that key; content is written verbatim in | ||||||||||||||||||||||||||||
| // plaintext. 0600 only guards against other local OS users — it does not protect | ||||||||||||||||||||||||||||
| // against this user's processes, backups, or synced folders. appID and key are | ||||||||||||||||||||||||||||
| // opaque strings: any "/" is escaped into a single path segment, never treated | ||||||||||||||||||||||||||||
| // as a directory separator. | ||||||||||||||||||||||||||||
| func Write(appID, key string, data []byte) error { | ||||||||||||||||||||||||||||
| if err := checkSeg(appID, "appID"); err != nil { | ||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if err := checkSeg(key, "key"); err != nil { | ||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if err := vfs.MkdirAll(appDir(appID), 0700); err != nil { | ||||||||||||||||||||||||||||
| return fmt.Errorf("apps storage: create dir: %w", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if err := validate.AtomicWrite(appKeyPath(appID, key), data, 0600); err != nil { | ||||||||||||||||||||||||||||
| return fmt.Errorf("apps storage: write: %w", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Delete removes the file under (appID, key). A missing file is not an error. | ||||||||||||||||||||||||||||
| func Delete(appID, key string) error { | ||||||||||||||||||||||||||||
| if err := checkSeg(appID, "appID"); err != nil { | ||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if err := checkSeg(key, "key"); err != nil { | ||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if err := vfs.Remove(appKeyPath(appID, key)); err != nil && !errors.Is(err, os.ErrNotExist) { | ||||||||||||||||||||||||||||
| return fmt.Errorf("apps storage: delete: %w", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // List returns the keys stored under appID, skipping subdirectories and names | ||||||||||||||||||||||||||||
| // that fail to unescape. A missing app directory yields an empty list. | ||||||||||||||||||||||||||||
| func List(appID string) ([]string, error) { | ||||||||||||||||||||||||||||
| if err := checkSeg(appID, "appID"); err != nil { | ||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| entries, err := vfs.ReadDir(appDir(appID)) | ||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||
| if errors.Is(err, os.ErrNotExist) { | ||||||||||||||||||||||||||||
| return []string{}, nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return nil, fmt.Errorf("apps storage: read dir: %w", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| keys := make([]string, 0, len(entries)) | ||||||||||||||||||||||||||||
| for _, e := range entries { | ||||||||||||||||||||||||||||
| if e.IsDir() { | ||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| key, err := url.PathUnescape(e.Name()) | ||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| keys = append(keys, key) | ||||||||||||||||||||||||||||
|
Comment on lines
+122
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Skip decoded names that fail After Suggested fix key, err := url.PathUnescape(e.Name())
if err != nil {
continue
}
+ if err := checkSeg(key, "key"); err != nil {
+ continue
+ }
keys = append(keys, key)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return keys, nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep
shortcuts/offinternal/vfs.This new package lives under
shortcuts/, so importinginternal/vfshere breaks the repo’s shortcut/runtime boundary. Please either move this storage layer out ofshortcuts/or route these filesystem operations through the existing runtime/file-IO abstraction instead of depending oninternal/vfsdirectly.Based on learnings: In the larksuite/cli repo, the golangci-lint depguard rule
shortcuts-no-vfsprohibits anything undershortcuts/from importinginternal/vfsdirectly; shortcuts should useruntime.FileIO()/runtime.ValidatePath()instead.🤖 Prompt for AI Agents