Merge branch 'feature/assets-s3-sharing'
CI / test (push) Successful in 3s

Integrate asset metadata/storage support, TypeScript client asset APIs, docs updates, and the Leaflet demo while resolving conflicts with recent challenge IP/login persistence changes on main.

Made-with: Cursor
This commit is contained in:
2026-03-02 21:23:31 +00:00
29 changed files with 2128 additions and 69 deletions
+106
View File
@@ -20,6 +20,8 @@ type MemoryStore struct {
invitations map[string]Invitation
collections map[string]Collection
features map[string]Feature
assets map[string]Asset
featureRefs map[string]map[string]FeatureAsset
}
func NewMemoryStore() *MemoryStore {
@@ -30,6 +32,8 @@ func NewMemoryStore() *MemoryStore {
invitations: make(map[string]Invitation),
collections: make(map[string]Collection),
features: make(map[string]Feature),
assets: make(map[string]Asset),
featureRefs: make(map[string]map[string]FeatureAsset),
}
}
@@ -166,6 +170,7 @@ func (s *MemoryStore) DeleteCollection(id string) error {
for fid, f := range s.features {
if f.CollectionID == id {
delete(s.features, fid)
delete(s.featureRefs, fid)
}
}
delete(s.collections, id)
@@ -207,9 +212,110 @@ func (s *MemoryStore) DeleteFeature(featureID string) error {
return ErrNotFound
}
delete(s.features, featureID)
delete(s.featureRefs, featureID)
return nil
}
func (s *MemoryStore) SaveAsset(a Asset) {
s.mu.Lock()
defer s.mu.Unlock()
s.assets[a.ID] = a
}
func (s *MemoryStore) GetAsset(assetID string) (Asset, error) {
s.mu.RLock()
defer s.mu.RUnlock()
a, ok := s.assets[assetID]
if !ok {
return Asset{}, ErrNotFound
}
return a, nil
}
func (s *MemoryStore) GetAssetByOwnerChecksumExt(ownerKey, checksum, ext string) (Asset, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, a := range s.assets {
if a.OwnerKey == ownerKey && a.Checksum == checksum && a.Ext == ext {
return a, nil
}
}
return Asset{}, ErrNotFound
}
func (s *MemoryStore) SetAssetPublic(assetID string, isPublic bool) error {
s.mu.Lock()
defer s.mu.Unlock()
a, ok := s.assets[assetID]
if !ok {
return ErrNotFound
}
a.IsPublic = isPublic
a.UpdatedAt = time.Now().UTC()
s.assets[assetID] = a
return nil
}
func (s *MemoryStore) LinkAssetToFeature(featureID, assetID, name, description string) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.features[featureID]; !ok {
return ErrNotFound
}
a, ok := s.assets[assetID]
if !ok {
return ErrNotFound
}
if _, ok := s.featureRefs[featureID]; !ok {
s.featureRefs[featureID] = make(map[string]FeatureAsset)
}
if existing, exists := s.featureRefs[featureID][assetID]; exists {
existing.Name = name
existing.Description = description
s.featureRefs[featureID][assetID] = existing
return nil
}
s.featureRefs[featureID][assetID] = FeatureAsset{
Asset: a,
FeatureID: featureID,
Name: name,
Description: description,
LinkedAt: time.Now().UTC(),
}
return nil
}
func (s *MemoryStore) UnlinkAssetFromFeature(featureID, assetID string) error {
s.mu.Lock()
defer s.mu.Unlock()
links, ok := s.featureRefs[featureID]
if !ok {
return ErrNotFound
}
if _, exists := links[assetID]; !exists {
return ErrNotFound
}
delete(links, assetID)
return nil
}
func (s *MemoryStore) ListAssetsByFeature(featureID string) []FeatureAsset {
s.mu.RLock()
defer s.mu.RUnlock()
links, ok := s.featureRefs[featureID]
if !ok {
return []FeatureAsset{}
}
result := make([]FeatureAsset, 0, len(links))
for assetID, fa := range links {
if updated, exists := s.assets[assetID]; exists {
fa.Asset = updated
}
result = append(result, fa)
}
return result
}
func (s *MemoryStore) PruneExpired(now time.Time) {
s.mu.Lock()
defer s.mu.Unlock()