Files
ttstd-infinite-canvas/backend/internal/service/protocol_plugins.go

725 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"infinite-canvas/backend/internal/payment"
"infinite-canvas/backend/internal/protocol"
)
const protocolPluginMaxBytes = protocol.PluginManifestMaxBytes
// PluginView is the backend representation consumed by the single frontend
// plugin center. Protocol-specific runtime data is nested under Protocol so
// the public plugin contract can grow without adding another center API.
type PluginView struct {
Manifest PluginManifestView `json:"manifest"`
Source string `json:"source"`
FileName string `json:"fileName"`
Package string `json:"package"`
SHA256 string `json:"sha256"`
InstalledAt time.Time `json:"installedAt"`
UpdatedAt time.Time `json:"updatedAt"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
Management PluginManagementView `json:"management"`
}
type PluginManifestView struct {
APIVersion string `json:"apiVersion"`
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Entry string `json:"entry,omitempty"`
Surfaces []string `json:"surfaces,omitempty"`
Description string `json:"description,omitempty"`
Documentation string `json:"documentation,omitempty"`
Author string `json:"author,omitempty"`
Permissions []string `json:"permissions"`
Trusted bool `json:"trusted"`
Runtime protocol.ManifestRuntime `json:"runtime,omitempty"`
Configuration protocol.ManifestConfiguration `json:"configuration,omitempty"`
Contributes protocol.ManifestContributions `json:"contributes"`
}
type pluginRecord struct {
Raw []byte
Metadata protocol.Metadata
Source string
FileName string
PackagePath string
PackageSHA256 string
SHA256 string
InstalledAt time.Time
UpdatedAt time.Time
Status string
Error string
}
type pluginRuntime struct {
mu sync.RWMutex
mutationMu sync.Mutex
registryPath string
packageDir string
plugins map[string]pluginRecord
registry *protocol.Registry
paymentRegistry *payment.Registry
}
type pluginRegistryRecord struct {
ID string `json:"id"`
Raw json.RawMessage `json:"manifest"`
Source string `json:"source"`
FileName string `json:"fileName,omitempty"`
PackagePath string `json:"packagePath,omitempty"`
PackageSHA256 string `json:"packageSha256,omitempty"`
InstalledAt time.Time `json:"installedAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func newPluginRuntime(dataDir string) (*pluginRuntime, error) {
if err := os.MkdirAll(dataDir, 0o755); err != nil {
return nil, fmt.Errorf("create plugin registry directory: %w", err)
}
packageDir := filepath.Join(dataDir, "plugin-packages")
if err := os.MkdirAll(packageDir, 0o700); err != nil {
return nil, fmt.Errorf("create plugin package directory: %w", err)
}
center := &pluginRuntime{registryPath: filepath.Join(dataDir, "plugin_registry.json"), packageDir: packageDir, plugins: make(map[string]pluginRecord)}
if err := center.bootstrapBuiltInPlugins(); err != nil {
return nil, err
}
if err := center.reload(); err != nil {
return nil, err
}
return center, nil
}
func (c *pluginRuntime) bootstrapBuiltInPlugins() error {
stored, err := c.readRegistry()
if err != nil {
return err
}
byID := make(map[string]pluginRegistryRecord, len(stored))
for _, record := range stored {
byID[record.ID] = record
}
officialDir, err := officialPluginPackageDir()
if err != nil {
return err
}
entries, err := os.ReadDir(officialDir)
if err != nil {
return fmt.Errorf("读取官方插件目录失败:%w", err)
}
builtInIDs := make(map[string]struct{}, len(entries)+2)
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".yingce-plugin") {
continue
}
packageData, err := os.ReadFile(filepath.Join(officialDir, entry.Name()))
if err != nil {
return fmt.Errorf("读取官方插件包 %s:%w", entry.Name(), err)
}
pkg, err := protocol.ParsePluginPackage(packageData)
if err != nil {
return fmt.Errorf("校验官方插件包 %s:%w", entry.Name(), err)
}
if strings.HasPrefix(strings.TrimSpace(pkg.Manifest.Runtime.Backend), "host:") {
return fmt.Errorf("官方插件 %q 不能依赖 host 执行器", pkg.Manifest.Metadata.ID)
}
if len(pkg.Manifest.Contributes.PaymentProviders) == 0 {
if _, err := protocol.LoadInstalledProviders(pkg.ManifestRaw, nil); err != nil {
return fmt.Errorf("加载官方插件 %q:%w", pkg.Manifest.Metadata.ID, err)
}
}
id := pkg.Manifest.Metadata.ID
if _, duplicate := builtInIDs[id]; duplicate {
return fmt.Errorf("官方插件 ID %q 重复", id)
}
builtInIDs[id] = struct{}{}
manifest := pkg.Manifest
record := byID[id]
if len(record.Raw) > 0 {
var previous protocol.Manifest
if err := json.Unmarshal(record.Raw, &previous); err == nil {
manifest.Metadata.Enabled = previous.Metadata.Enabled
}
}
manifestData, err := json.Marshal(manifest)
if err != nil {
return fmt.Errorf("编码官方插件 %q:%w", id, err)
}
hash := pluginHash(packageData)
packageName := hash + ".yingce-plugin"
if err := writePluginFile(filepath.Join(c.packageDir, packageName), packageData); err != nil {
return fmt.Errorf("缓存官方插件 %q:%w", id, err)
}
now := time.Now().UTC()
if record.InstalledAt.IsZero() {
record.InstalledAt = now
}
source := PluginOriginOfficial
if _, systemPayment := systemPaymentPolicies[id]; systemPayment {
source = PluginOriginSystem
}
record.ID, record.Raw, record.Source, record.FileName = id, manifestData, source, entry.Name()
record.PackagePath, record.PackageSHA256, record.UpdatedAt = packageName, hash, now
byID[id] = record
}
bundledManifests := append(bundledWorkflowPluginManifests(), bundledPaymentPluginManifests()...)
for _, bundled := range bundledManifests {
builtInIDs[bundled.Metadata.ID] = struct{}{}
if existing, exists := byID[bundled.Metadata.ID]; exists && existing.PackagePath != "" && isSystemPaymentPluginID(bundled.Metadata.ID) {
// An official package is the executable source of truth. The bundled
// manifest remains only as a fallback when the package is unavailable.
continue
}
data, err := json.Marshal(bundled)
if err != nil {
return fmt.Errorf("encode built-in plugin %s: %w", bundled.Metadata.ID, err)
}
record := byID[bundled.Metadata.ID]
if len(record.Raw) > 0 {
var installed protocol.Manifest
if err := json.Unmarshal(record.Raw, &installed); err != nil {
return fmt.Errorf("decode built-in plugin %s: %w", bundled.Metadata.ID, err)
}
bundled.Metadata.Enabled = installed.Metadata.Enabled
data, err = json.Marshal(bundled)
if err != nil {
return fmt.Errorf("encode built-in plugin %s: %w", bundled.Metadata.ID, err)
}
}
now := time.Now().UTC()
if record.InstalledAt.IsZero() {
record.InstalledAt = now
}
source := PluginOriginOfficial
if _, systemPayment := systemPaymentPolicies[bundled.Metadata.ID]; systemPayment {
source = PluginOriginSystem
}
record.UpdatedAt = now
record.ID, record.Raw, record.Source, record.PackagePath = bundled.Metadata.ID, data, source, ""
byID[bundled.Metadata.ID] = record
}
result := make([]pluginRegistryRecord, 0, len(byID))
for _, record := range byID {
if isBuiltInPluginSource(record.Source) {
if _, exists := builtInIDs[record.ID]; !exists {
// Built-in records are reconciled from repository packages and host
// manifests on every startup, so removed plugins cannot survive stale.
continue
}
}
result = append(result, record)
}
sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID })
return c.writeRegistry(result)
}
func isSystemPaymentPluginID(id string) bool {
_, ok := systemPaymentPolicies[strings.TrimSpace(id)]
return ok
}
func isBuiltInPluginSource(source string) bool {
switch strings.TrimSpace(source) {
case "bundled", PluginOriginOfficial, PluginOriginSystem:
return true
default:
return false
}
}
func officialPluginPackageDir() (string, error) {
if configured := strings.TrimSpace(os.Getenv("CANVAS_OFFICIAL_PLUGIN_DIR")); configured != "" {
info, err := os.Stat(configured)
if err != nil || !info.IsDir() {
return "", fmt.Errorf("CANVAS_OFFICIAL_PLUGIN_DIR 不是可读目录:%s", configured)
}
return configured, nil
}
workingDir, err := os.Getwd()
if err != nil {
return "", err
}
candidates := []string{"/app/plugin-packages"}
current := workingDir
for range 8 {
candidates = append(candidates, filepath.Join(current, "plugin-packages"))
parent := filepath.Dir(current)
if parent == current {
break
}
current = parent
}
for _, candidate := range candidates {
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
return candidate, nil
}
}
return "", errors.New("未找到官方 plugin-packages 目录;请设置 CANVAS_OFFICIAL_PLUGIN_DIR")
}
func (c *pluginRuntime) reload() error {
c.mu.Lock()
defer c.mu.Unlock()
stored, err := c.readRegistry()
if err != nil {
return err
}
plugins := make(map[string]pluginRecord)
for _, storedRecord := range stored {
data := storedRecord.Raw
if len(data) > protocolPluginMaxBytes {
return fmt.Errorf("plugin %s exceeds %d bytes", storedRecord.ID, protocolPluginMaxBytes)
}
var manifest protocol.Manifest
if err := json.Unmarshal(data, &manifest); err != nil {
return fmt.Errorf("decode plugin %s: %w", storedRecord.ID, err)
}
metadata := manifest.Metadata
if strings.TrimSpace(metadata.ID) == "" {
return fmt.Errorf("plugin %s has no metadata id", storedRecord.ID)
}
if _, exists := plugins[metadata.ID]; exists {
return fmt.Errorf("duplicate installed protocol %q", metadata.ID)
}
packageSHA256 := storedRecord.PackageSHA256
if packageSHA256 == "" {
packageSHA256 = pluginHash(data)
}
plugins[metadata.ID] = pluginRecord{Raw: data, Metadata: metadata, Source: storedRecord.Source, FileName: storedRecord.FileName, PackagePath: storedRecord.PackagePath, PackageSHA256: packageSHA256, SHA256: packageSHA256, InstalledAt: storedRecord.InstalledAt, UpdatedAt: storedRecord.UpdatedAt, Status: "invalid"}
}
registry, err := protocol.NewRegistry()
if err != nil {
return err
}
for id, record := range plugins {
var manifest protocol.Manifest
if err := json.Unmarshal(record.Raw, &manifest); err != nil {
record.Error = err.Error()
plugins[id] = record
continue
}
if len(manifest.Contributes.PaymentProviders) > 0 && record.Source == PluginOriginUploaded {
backend := strings.TrimSpace(manifest.Runtime.Backend)
if strings.HasPrefix(backend, "host:") {
record.Metadata.Enabled = false
record.Status = "invalid"
record.Error = "首期支付插件只能使用系统宿主适配器"
plugins[id] = record
continue
}
if backend != "rpc" && backend != "wasm" {
record.Metadata.Enabled = false
record.Status = "invalid"
record.Error = "上传支付插件必须声明 rpc 或 wasm 后端"
plugins[id] = record
continue
}
}
if len(manifest.Contributes.PaymentProviders) > 0 && len(manifest.Contributes.Providers) == 0 {
if record.Metadata.Enabled {
record.Status = "enabled"
} else {
record.Status = "disabled"
}
plugins[id] = record
continue
}
adapters, loadErr := protocol.LoadInstalledProviders(record.Raw, nil)
if loadErr != nil {
record.Metadata.Enabled = false
record.Metadata.UnavailableReason = loadErr.Error()
record.Error = loadErr.Error()
_ = registry.Register(protocol.UnavailableAdapter{Info: record.Metadata})
plugins[id] = record
continue
}
if !record.Metadata.Enabled {
record.Status = "disabled"
for _, adapter := range adapters {
info := adapter.Metadata()
info.Enabled = false
_ = registry.Register(protocol.UnavailableAdapter{Info: info})
}
plugins[id] = record
continue
}
registrationFailed := false
for _, adapter := range adapters {
if err := registry.Register(adapter); err != nil {
record.Error = err.Error()
registrationFailed = true
}
}
if registrationFailed {
plugins[id] = record
continue
}
record.Status = "enabled"
plugins[id] = record
}
c.plugins = plugins
c.registry = registry
// Payment adapters are loaded exclusively from validated plugin packages.
// An empty registry is intentional when the official package is unavailable;
// payment writes must fail closed instead of silently using host code.
basePayment, registryErr := payment.NewRegistry()
if registryErr != nil {
return registryErr
}
dynamicPayments := make([]payment.Provider, 0)
for id, record := range plugins {
if (record.Source != PluginOriginUploaded && record.Source != PluginOriginOfficial && record.Source != PluginOriginSystem) || !record.Metadata.Enabled || record.Status != "enabled" {
continue
}
var manifest protocol.Manifest
if err := json.Unmarshal(record.Raw, &manifest); err != nil || len(manifest.Contributes.PaymentProviders) == 0 {
continue
}
if strings.TrimSpace(manifest.Runtime.Backend) != "rpc" {
record.Status = "invalid"
record.Error = "wasm 支付运行时尚未启用"
plugins[id] = record
continue
}
packageData, readErr := os.ReadFile(filepath.Join(c.packageDir, filepath.Base(record.PackagePath)))
if readErr != nil {
record.Status = "invalid"
record.Error = "支付插件包文件不存在"
plugins[id] = record
continue
}
pkg, parseErr := protocol.ParsePluginPackage(packageData)
if parseErr != nil {
record.Status = "invalid"
record.Error = parseErr.Error()
plugins[id] = record
continue
}
runtimeDir, materializeErr := materializePaymentBackend(c.packageDir, record.PackageSHA256, pkg)
if materializeErr != nil {
record.Status = "invalid"
record.Error = materializeErr.Error()
plugins[id] = record
continue
}
for _, contribution := range manifest.Contributes.PaymentProviders {
provider, providerErr := payment.NewRPCProvider(payment.DescriptorFromManifest(manifest, contribution), runtimeDir, manifest.Runtime.BackendEntry)
if providerErr != nil {
record.Status = "invalid"
record.Error = providerErr.Error()
continue
}
dynamicPayments = append(dynamicPayments, provider)
}
}
dynamicIDs := make(map[string]struct{}, len(dynamicPayments))
for _, provider := range dynamicPayments {
dynamicIDs[provider.Descriptor().ID] = struct{}{}
}
baseProviders := make([]payment.Provider, 0)
for _, provider := range basePayment.Providers() {
if _, replaced := dynamicIDs[provider.Descriptor().ID]; !replaced {
baseProviders = append(baseProviders, provider)
}
}
providers := append(baseProviders, dynamicPayments...)
c.paymentRegistry, err = payment.NewRegistry(providers...)
if err != nil {
return err
}
c.plugins = plugins
return nil
}
func materializePaymentBackend(packageDir, hash string, pkg protocol.PluginPackage) (string, error) {
root := filepath.Join(packageDir, "runtime", strings.TrimSpace(hash))
if err := os.MkdirAll(root, 0o700); err != nil {
return "", fmt.Errorf("创建支付插件运行目录失败:%w", err)
}
for name, content := range pkg.Files {
if !strings.HasPrefix(name, "backend/") || strings.HasSuffix(name, "/") {
continue
}
target := filepath.Join(root, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
return "", err
}
if err := os.WriteFile(target, content, 0o700); err != nil {
return "", fmt.Errorf("写入支付插件运行文件失败:%w", err)
}
}
return root, nil
}
func (c *pluginRuntime) list() []PluginView {
c.mu.RLock()
defer c.mu.RUnlock()
items := make([]PluginView, 0, len(c.plugins))
for _, item := range c.plugins {
items = append(items, PluginView{Manifest: pluginManifestView(item.Raw, item.Metadata, item.Source), Source: item.Source, FileName: item.FileName, Package: protocol.PluginPackageFormat, SHA256: item.SHA256, InstalledAt: item.InstalledAt, UpdatedAt: item.UpdatedAt, Status: item.Status, Error: item.Error})
}
sort.Slice(items, func(i, j int) bool { return items[i].Manifest.ID < items[j].Manifest.ID })
return items
}
func (c *pluginRuntime) registrySnapshot() *protocol.Registry {
c.mu.RLock()
defer c.mu.RUnlock()
return c.registry
}
func (c *pluginRuntime) paymentRegistrySnapshot() *payment.Registry {
c.mu.RLock()
defer c.mu.RUnlock()
return c.paymentRegistry
}
func (c *pluginRuntime) install(data []byte, fileName string) (PluginView, error) {
c.mutationMu.Lock()
defer c.mutationMu.Unlock()
if len(data) == 0 || len(data) > protocol.PluginPackageMaxBytes {
return PluginView{}, fmt.Errorf("plugin package must be between 1 and %d bytes", protocol.PluginPackageMaxBytes)
}
pkg, err := protocol.ParsePluginPackage(data)
if err != nil {
return PluginView{}, err
}
manifest := pkg.Manifest
if strings.HasPrefix(strings.TrimSpace(manifest.Runtime.Backend), "host:") {
return PluginView{}, errors.New("上传插件不能使用宿主内置执行器")
}
if len(manifest.Contributes.PaymentProviders) == 0 {
if _, err := protocol.LoadInstalledProviders(pkg.ManifestRaw, nil); err != nil {
return PluginView{}, err
}
}
c.mu.RLock()
existing, exists := c.plugins[manifest.Metadata.ID]
c.mu.RUnlock()
if exists && isBuiltInPluginSource(existing.Source) && !isPaymentPluginManifest(manifest) {
return PluginView{}, fmt.Errorf("内置插件 %q 不能通过上传覆盖", manifest.Metadata.ID)
}
manifest.Metadata.Enabled = !exists || existing.Metadata.Enabled
manifestData, err := json.Marshal(manifest)
if err != nil {
return PluginView{}, err
}
hash := pluginHash(data)
packageName := filepath.Base(strings.TrimSpace(fileName))
if packageName == "." || packageName == "" || packageName == string(filepath.Separator) {
packageName = manifest.Metadata.ID + ".yingce-plugin"
}
packagePath := filepath.Join(c.packageDir, hash+".yingce-plugin")
if err := writePluginFile(packagePath, data); err != nil {
return PluginView{}, fmt.Errorf("保存插件包失败:%w", err)
}
stored, err := c.readRegistry()
if err != nil {
_ = os.Remove(packagePath)
return PluginView{}, err
}
previousStored := append([]pluginRegistryRecord(nil), stored...)
now := time.Now().UTC()
newRecord := pluginRegistryRecord{ID: manifest.Metadata.ID, Raw: manifestData, Source: "uploaded", FileName: packageName, PackagePath: filepath.Base(packagePath), PackageSHA256: hash, InstalledAt: now, UpdatedAt: now}
if exists {
newRecord.InstalledAt = existing.InstalledAt
for index := range stored {
if stored[index].ID == manifest.Metadata.ID {
stored[index] = newRecord
break
}
}
} else {
stored = append(stored, newRecord)
}
if err := c.writeRegistry(stored); err != nil {
_ = os.Remove(packagePath)
return PluginView{}, fmt.Errorf("保存插件失败:%w", err)
}
if err := c.reload(); err != nil {
_ = c.writeRegistry(previousStored)
_ = c.reload()
_ = os.Remove(packagePath)
return PluginView{}, err
}
if exists && existing.PackagePath != "" && existing.PackagePath != filepath.Base(packagePath) {
_ = os.Remove(filepath.Join(c.packageDir, filepath.Base(existing.PackagePath)))
}
for _, item := range c.list() {
if item.Manifest.ID == manifest.Metadata.ID {
return item, nil
}
}
return PluginView{}, errors.New("插件保存后未加载")
}
func isPaymentPluginManifest(manifest protocol.Manifest) bool {
backend := strings.TrimSpace(manifest.Runtime.Backend)
return len(manifest.Contributes.PaymentProviders) > 0 && (backend == "rpc" || backend == "wasm")
}
func (c *pluginRuntime) setEnabled(id string, enabled bool) (PluginView, error) {
c.mutationMu.Lock()
defer c.mutationMu.Unlock()
c.mu.RLock()
record, ok := c.plugins[strings.TrimSpace(id)]
c.mu.RUnlock()
if !ok {
return PluginView{}, fmt.Errorf("插件 %q 不存在", id)
}
var manifest protocol.Manifest
if err := json.Unmarshal(record.Raw, &manifest); err != nil {
return PluginView{}, err
}
manifest.Metadata.Enabled = enabled
data, err := json.Marshal(manifest)
if err != nil {
return PluginView{}, err
}
stored, err := c.readRegistry()
if err != nil {
return PluginView{}, err
}
for index := range stored {
if stored[index].ID == record.Metadata.ID {
stored[index].Raw = data
stored[index].UpdatedAt = time.Now().UTC()
}
}
if err := c.writeRegistry(stored); err != nil {
return PluginView{}, err
}
if err := c.reload(); err != nil {
return PluginView{}, err
}
for _, item := range c.list() {
if item.Manifest.ID == manifest.Metadata.ID {
return item, nil
}
}
return PluginView{}, errors.New("插件状态更新后未加载")
}
func pluginManifestView(raw []byte, metadata protocol.Metadata, source string) PluginManifestView {
var manifest protocol.Manifest
_ = json.Unmarshal(raw, &manifest)
if manifest.Metadata.ID == "" {
manifest.Metadata = metadata
}
return PluginManifestView{
ID: metadata.ID, Name: metadata.Name, Version: metadata.Version, APIVersion: "yingce.plugin/v1", Entry: manifest.Entry, Surfaces: manifest.Surfaces,
Description: metadata.Description, Documentation: metadata.Documentation, Author: metadata.Vendor,
Permissions: manifest.Permissions, Trusted: isBuiltInPluginSource(source), Runtime: manifest.Runtime,
Configuration: manifest.Configuration, Contributes: manifest.Contributes,
}
}
func (c *pluginRuntime) uninstall(id string) error {
c.mutationMu.Lock()
defer c.mutationMu.Unlock()
c.mu.RLock()
record, ok := c.plugins[strings.TrimSpace(id)]
c.mu.RUnlock()
if !ok {
return fmt.Errorf("插件 %q 不存在", id)
}
if isBuiltInPluginSource(record.Source) {
return fmt.Errorf("内置插件 %q 不能卸载,可停用该插件", id)
}
stored, err := c.readRegistry()
if err != nil {
return err
}
filtered := stored[:0]
for _, item := range stored {
if item.ID != record.Metadata.ID {
filtered = append(filtered, item)
}
}
if err := c.writeRegistry(filtered); err != nil {
return err
}
if err := c.reload(); err != nil {
return err
}
if record.PackagePath != "" {
_ = os.Remove(filepath.Join(c.packageDir, filepath.Base(record.PackagePath)))
}
return nil
}
func (c *pluginRuntime) readRegistry() ([]pluginRegistryRecord, error) {
data, err := os.ReadFile(c.registryPath)
if errors.Is(err, os.ErrNotExist) {
return []pluginRegistryRecord{}, nil
}
if err != nil {
return nil, err
}
if len(data) > protocolPluginMaxBytes*64 {
return nil, fmt.Errorf("插件 registry 超过大小限制")
}
var records []pluginRegistryRecord
if err := json.Unmarshal(data, &records); err != nil {
return nil, fmt.Errorf("读取插件 registry 失败:%w", err)
}
return records, nil
}
func (c *pluginRuntime) writeRegistry(records []pluginRegistryRecord) error {
data, err := json.MarshalIndent(records, "", " ")
if err != nil {
return err
}
return writePluginFile(c.registryPath, data)
}
func writePluginFile(path string, data []byte) error {
temporary, err := os.CreateTemp(filepath.Dir(path), ".plugin-*.tmp")
if err != nil {
return err
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if err := temporary.Chmod(0o600); err != nil {
_ = temporary.Close()
return err
}
if _, err := temporary.Write(data); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Close(); err != nil {
return err
}
return os.Rename(temporaryPath, path)
}
func pluginSource(metadata protocol.Metadata) string {
return "uploaded"
}
func pluginHash(data []byte) string {
digest := sha256.Sum256(data)
return hex.EncodeToString(digest[:])
}