github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/assets/asset.go (about)

     1  package assets
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/cozy/cozy-stack/pkg/assets/dynamic"
    13  	"github.com/cozy/cozy-stack/pkg/assets/model"
    14  	"github.com/cozy/cozy-stack/pkg/assets/statik"
    15  	"github.com/cozy/cozy-stack/pkg/config/config"
    16  	"github.com/cozy/cozy-stack/pkg/i18n"
    17  	"github.com/cozy/cozy-stack/pkg/logger"
    18  )
    19  
    20  // Get looks for an asset. It tries in this order:
    21  // 1. A dynamic asset for the given context
    22  // 2. A dynamic asset for the default context
    23  // 3. A static asset.
    24  func Get(name, context string) (*model.Asset, bool) {
    25  	if context == "" {
    26  		context = config.DefaultInstanceContext
    27  	}
    28  
    29  	// Check if a dynamic asset is existing
    30  	dynAsset, err := dynamic.GetAsset(context, name)
    31  	if err == nil {
    32  		return dynAsset, true
    33  	}
    34  	if !errors.Is(err, dynamic.ErrDynAssetNotFound) {
    35  		logger.WithNamespace("asset").Errorf("Error while retreiving dynamic asset: %s", err)
    36  	}
    37  
    38  	if context != config.DefaultInstanceContext {
    39  		dynAsset, err = dynamic.GetAsset(config.DefaultInstanceContext, name)
    40  		if err == nil {
    41  			return dynAsset, true
    42  		}
    43  	}
    44  
    45  	// If asset was not found, try to retrieve it from static assets
    46  	asset := statik.GetAsset(name)
    47  	if asset == nil {
    48  		return nil, false
    49  	}
    50  	return asset, true
    51  }
    52  
    53  // Head does the same job as Get, but the returned model.Asset can have no body
    54  // data. It allows to use a cache for it.
    55  func Head(name, context string) (*model.Asset, bool) {
    56  	if context == "" {
    57  		context = config.DefaultInstanceContext
    58  	}
    59  	key := fmt.Sprintf("dyn-assets:%s/%s", context, name)
    60  	cache := config.GetConfig().CacheStorage
    61  	if data, ok := cache.Get(key); ok {
    62  		asset := &model.Asset{}
    63  		if err := json.Unmarshal(data, asset); err == nil {
    64  			return asset, true
    65  		}
    66  	}
    67  	asset, ok := Get(name, context)
    68  	if !ok {
    69  		return nil, false
    70  	}
    71  	if data, err := json.Marshal(asset); err == nil {
    72  		cache.Set(key, data, 24*time.Hour)
    73  	}
    74  	return asset, true
    75  }
    76  
    77  // Add adds dynamic assets
    78  func Add(options []model.AssetOption) error {
    79  	err := dynamic.RegisterCustomExternals(options, 0)
    80  	if err == nil {
    81  		cache := config.GetConfig().CacheStorage
    82  		for _, opt := range options {
    83  			key := fmt.Sprintf("dyn-assets:%s/%s", opt.Context, opt.Name)
    84  			cache.Clear(key)
    85  		}
    86  	}
    87  	return err
    88  }
    89  
    90  // Remove removes an asset
    91  // Note: Only dynamic assets can be removed
    92  func Remove(name, context string) error {
    93  	err := dynamic.RemoveAsset(context, name)
    94  	if err == nil {
    95  		key := fmt.Sprintf("dyn-assets:%s/%s", context, name)
    96  		cache := config.GetConfig().CacheStorage
    97  		cache.Clear(key)
    98  	}
    99  	return err
   100  }
   101  
   102  // List returns a map containing all the existing assets (statik & dynamic)
   103  // Each map key represents the context
   104  func List() (map[string][]*model.Asset, error) {
   105  	assetsMap := make(map[string][]*model.Asset)
   106  
   107  	defctx := config.DefaultInstanceContext
   108  
   109  	// Get dynamic assets
   110  	dynAssets, err := dynamic.ListAssets()
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	for ctx, assets := range dynAssets {
   115  		assetsMap[ctx] = append(assetsMap[ctx], assets...)
   116  	}
   117  
   118  	// Get statik assets
   119  	statik.Foreach(func(name string, f *model.Asset) {
   120  		for _, asset := range assetsMap[defctx] {
   121  			if asset.Name == f.Name {
   122  				return
   123  			}
   124  		}
   125  		assetsMap[defctx] = append(assetsMap[defctx], f)
   126  	})
   127  
   128  	return assetsMap, nil
   129  }
   130  
   131  // Open returns a bytes.Reader for an asset in the given context, or the
   132  // default context if no context is given.
   133  func Open(name string, context string) (*bytes.Reader, error) {
   134  	f, ok := Get(name, context)
   135  	if ok {
   136  		return f.Reader(), nil
   137  	}
   138  	return nil, os.ErrNotExist
   139  }
   140  
   141  // LoadContextualizedLocale loads the translations dictionary from dynamic
   142  // assets for the given locale and context.
   143  func LoadContextualizedLocale(context, locale string) {
   144  	name := "/locales/" + locale + ".po"
   145  	asset, ok := Head(name, context)
   146  	if !ok || asset.Context != context {
   147  		return
   148  	}
   149  	f, err := Open(name, context)
   150  	if err != nil {
   151  		return
   152  	}
   153  	buf, err := io.ReadAll(f)
   154  	if err == nil {
   155  		i18n.LoadLocale(locale, context, buf)
   156  	}
   157  }