Files
aego/app/edit/remote_examples.go
sanspie a46347e2c6
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 23m57s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
editor: add art studio and stage 12.5 workflows
2026-11-08 18:47:57 +03:00

194 lines
5.4 KiB
Go

package edit
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"aego/app/assembly/builtin"
"aego/editor/shell/launcher"
"aego/engine"
"aego/project/remoteexample"
)
// launcherRemote only orchestrates the existing checked store and HTTPS
// transport. It never starts a process or executes project code.
type launcherRemote struct {
root string
clientFactory func() (*remoteexample.Client, error)
}
func (r launcherRemote) storeRoot() (string, error) {
root := r.root
if root == "" {
config, err := os.UserConfigDir()
if err != nil {
return "", err
}
root = filepath.Join(config, "aego", "remote-examples")
}
return filepath.Abs(root)
}
func (r launcherRemote) store() (*remoteexample.Store, error) {
root, err := r.storeRoot()
if err != nil {
return nil, err
}
return remoteexample.NewStore(root, remoteexample.Limits{})
}
// existingProject recognizes managed copies even when opened via Recent or a
// path, so those routes do not accidentally drop the remote import safeguards.
// A project elsewhere is local and does not create or initialize the store.
func (r launcherRemote) existingProject(root string) (launcher.RemoteCopy, bool, error) {
storeRoot, err := r.storeRoot()
if err != nil {
return launcher.RemoteCopy{}, false, err
}
resolvedStore, err := filepath.EvalSymlinks(storeRoot)
if errors.Is(err, os.ErrNotExist) {
return launcher.RemoteCopy{}, false, nil
}
if err != nil {
return launcher.RemoteCopy{}, false, err
}
resolvedRoot, err := filepath.EvalSymlinks(root)
if err != nil {
return launcher.RemoteCopy{}, false, err
}
rel, err := filepath.Rel(filepath.Join(resolvedStore, "projects"), resolvedRoot)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return launcher.RemoteCopy{}, false, nil
}
if rel == "." || !filepath.IsLocal(rel) || strings.ContainsRune(rel, filepath.Separator) {
return launcher.RemoteCopy{}, true, fmt.Errorf("invalid project path inside remote example store")
}
store, err := r.store()
if err != nil {
return launcher.RemoteCopy{}, true, err
}
copy, err := store.Reopen(rel)
if err != nil {
return launcher.RemoteCopy{}, true, err
}
return launcherCopy(copy), true, nil
}
func (r launcherRemote) client() (*remoteexample.Client, error) {
if r.clientFactory != nil {
return r.clientFactory()
}
return remoteexample.NewClient(remoteexample.Policy{EngineVersion: engine.Version, InstalledModules: builtin.Default().IDs()})
}
func launcherCopy(c remoteexample.Copy) launcher.RemoteCopy {
return launcher.RemoteCopy{ID: c.ID, Name: c.Manifest.ExampleID, Version: c.Manifest.Version, Source: c.ManifestURL, Directory: c.Directory, Entry: c.Entry}
}
func (r launcherRemote) List(ctx context.Context) ([]launcher.RemoteCopy, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
store, err := r.store()
if err != nil {
return nil, err
}
copies, err := store.Copies()
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
if len(copies) > launcher.MaxRecent {
copies = copies[:launcher.MaxRecent]
}
out := make([]launcher.RemoteCopy, 0, len(copies))
for _, c := range copies {
out = append(out, launcherCopy(c))
}
return out, nil
}
func (r launcherRemote) Open(ctx context.Context, source, approved string, clone bool) (launcher.RemoteCopy, error) {
if err := ctx.Err(); err != nil {
return launcher.RemoteCopy{}, err
}
origin, err := remoteexample.Origin(source)
if err != nil || approved != origin {
return launcher.RemoteCopy{}, fmt.Errorf("explicit confirmation of the exact HTTPS origin is required")
}
store, err := r.store()
if err != nil {
return launcher.RemoteCopy{}, err
}
if !clone {
copy, err := store.OpenRecent(source)
if err == nil {
return launcherCopy(copy), ctx.Err()
}
if !errors.Is(err, os.ErrNotExist) {
return launcher.RemoteCopy{}, err
}
}
client, err := r.client()
if err != nil {
return launcher.RemoteCopy{}, err
}
defer client.Close()
copy, err := store.Clone(ctx, client, source, approved)
if err != nil {
return launcher.RemoteCopy{}, err
}
return launcherCopy(copy), ctx.Err()
}
func (r launcherRemote) Reopen(ctx context.Context, id string) (launcher.RemoteCopy, error) {
if err := ctx.Err(); err != nil {
return launcher.RemoteCopy{}, err
}
store, err := r.store()
if err != nil {
return launcher.RemoteCopy{}, err
}
copy, err := store.Reopen(id)
if err != nil {
return launcher.RemoteCopy{}, err
}
return launcherCopy(copy), ctx.Err()
}
func (r launcherRemote) Check(ctx context.Context, id, approved string) (launcher.RemoteUpdate, error) {
if err := ctx.Err(); err != nil {
return launcher.RemoteUpdate{}, err
}
store, err := r.store()
if err != nil {
return launcher.RemoteUpdate{}, err
}
copy, err := store.Reopen(id)
if err != nil {
return launcher.RemoteUpdate{}, err
}
origin, err := remoteexample.Origin(copy.ManifestURL)
if err != nil || approved != origin {
return launcher.RemoteUpdate{}, fmt.Errorf("explicit confirmation of the exact HTTPS origin is required")
}
client, err := r.client()
if err != nil {
return launcher.RemoteUpdate{}, err
}
defer client.Close()
update, err := client.CheckUpdate(ctx, copy, approved)
if err != nil {
return launcher.RemoteUpdate{}, err
}
return launcher.RemoteUpdate{Available: update.Available, Current: update.Current, Remote: update.Remote}, nil
}
var _ launcher.RemoteExamples = launcherRemote{}