github.com/prebid/prebid-server/v2@v2.18.0/stored_requests/backends/file_fetcher/fetcher.go (about)

     1  package file_fetcher
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/prebid/prebid-server/v2/stored_requests"
    12  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    13  	jsonpatch "gopkg.in/evanphx/json-patch.v4"
    14  )
    15  
    16  // NewFileFetcher _immediately_ loads stored request data from local files.
    17  // These are stored in memory for low-latency reads.
    18  //
    19  // This expects each file in the directory to be named "{config_id}.json".
    20  // For example, when asked to fetch the request with ID == "23", it will return the data from "directory/23.json".
    21  func NewFileFetcher(directory string) (stored_requests.AllFetcher, error) {
    22  	storedData, err := collectStoredData(directory, FileSystem{make(map[string]FileSystem), make(map[string]json.RawMessage)}, nil)
    23  	return &eagerFetcher{storedData, nil}, err
    24  }
    25  
    26  type eagerFetcher struct {
    27  	FileSystem FileSystem
    28  	Categories map[string]map[string]stored_requests.Category
    29  }
    30  
    31  func (fetcher *eagerFetcher) FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (map[string]json.RawMessage, map[string]json.RawMessage, []error) {
    32  	storedRequests := fetcher.FileSystem.Directories["stored_requests"].Files
    33  	storedImpressions := fetcher.FileSystem.Directories["stored_imps"].Files
    34  	errs := appendErrors("Request", requestIDs, storedRequests, nil)
    35  	errs = appendErrors("Imp", impIDs, storedImpressions, errs)
    36  	return storedRequests, storedImpressions, errs
    37  }
    38  
    39  // Fetch Responses - Implements the interface to read the stored response information from the fetcher's FileSystem, the directory name is "stored_responses"
    40  func (fetcher *eagerFetcher) FetchResponses(ctx context.Context, ids []string) (data map[string]json.RawMessage, errs []error) {
    41  	storedRespFS, found := fetcher.FileSystem.Directories["stored_responses"]
    42  	if !found {
    43  		return nil, append(errs, errors.New(`no "stored_responses" directory found`))
    44  	}
    45  
    46  	data = storedRespFS.Files
    47  	return data, appendErrors("Response", ids, data, nil)
    48  }
    49  
    50  // FetchAccount fetches the host account configuration for a publisher
    51  func (fetcher *eagerFetcher) FetchAccount(ctx context.Context, accountDefaultsJSON json.RawMessage, accountID string) (json.RawMessage, []error) {
    52  	if len(accountID) == 0 {
    53  		return nil, []error{fmt.Errorf("Cannot look up an empty accountID")}
    54  	}
    55  	accountJSON, ok := fetcher.FileSystem.Directories["accounts"].Files[accountID]
    56  	if !ok {
    57  		return nil, []error{stored_requests.NotFoundError{
    58  			ID:       accountID,
    59  			DataType: "Account",
    60  		}}
    61  	}
    62  
    63  	completeJSON, err := jsonpatch.MergePatch(accountDefaultsJSON, accountJSON)
    64  	if err != nil {
    65  		return nil, []error{err}
    66  	}
    67  	return completeJSON, nil
    68  }
    69  
    70  func (fetcher *eagerFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) {
    71  	fileName := primaryAdServer
    72  
    73  	if len(publisherId) != 0 {
    74  		fileName = primaryAdServer + "_" + publisherId
    75  	}
    76  
    77  	if fetcher.Categories == nil {
    78  		fetcher.Categories = make(map[string]map[string]stored_requests.Category)
    79  	}
    80  	if data, ok := fetcher.Categories[fileName]; ok {
    81  		return data[iabCategory].Id, nil
    82  	}
    83  
    84  	if primaryAdServerDir, found := fetcher.FileSystem.Directories[primaryAdServer]; found {
    85  
    86  		if file, ok := primaryAdServerDir.Files[fileName]; ok {
    87  
    88  			tmp := make(map[string]stored_requests.Category)
    89  
    90  			if err := jsonutil.UnmarshalValid(file, &tmp); err != nil {
    91  				return "", fmt.Errorf("Unable to unmarshal categories for adserver: '%s', publisherId: '%s'", primaryAdServer, publisherId)
    92  			}
    93  			fetcher.Categories[fileName] = tmp
    94  			resultCategory := tmp[iabCategory].Id
    95  			primaryAdServerDir.Files[fileName] = nil
    96  
    97  			if len(resultCategory) == 0 {
    98  				return "", fmt.Errorf("Unable to find category for adserver '%s', publisherId: '%s', iab category: '%s'", primaryAdServer, publisherId, iabCategory)
    99  			}
   100  			return resultCategory, nil
   101  		} else {
   102  			return "", fmt.Errorf("Unable to find mapping file for adserver: '%s', publisherId: '%s'", primaryAdServer, publisherId)
   103  
   104  		}
   105  
   106  	}
   107  
   108  	return "", fmt.Errorf("Category '%s' not found for server: '%s', publisherId: '%s'",
   109  		iabCategory, primaryAdServer, publisherId)
   110  
   111  }
   112  
   113  type FileSystem struct {
   114  	Directories map[string]FileSystem
   115  	Files       map[string]json.RawMessage
   116  }
   117  
   118  func collectStoredData(directory string, fileSystem FileSystem, err error) (FileSystem, error) {
   119  	if err != nil {
   120  		return FileSystem{nil, nil}, err
   121  	}
   122  	fileInfos, err := os.ReadDir(directory)
   123  	if err != nil {
   124  		return FileSystem{nil, nil}, err
   125  	}
   126  	data := make(map[string]json.RawMessage)
   127  
   128  	for _, fileInfo := range fileInfos {
   129  		if fileInfo.IsDir() {
   130  
   131  			fs := FileSystem{make(map[string]FileSystem), make(map[string]json.RawMessage)}
   132  			fileSys, innerErr := collectStoredData(directory+"/"+fileInfo.Name(), fs, err)
   133  			if innerErr != nil {
   134  				return FileSystem{nil, nil}, innerErr
   135  			}
   136  			fileSystem.Directories[fileInfo.Name()] = fileSys
   137  
   138  		} else {
   139  			if strings.HasSuffix(fileInfo.Name(), ".json") { // Skip the .gitignore
   140  				fileData, err := os.ReadFile(fmt.Sprintf("%s/%s", directory, fileInfo.Name()))
   141  				if err != nil {
   142  					return FileSystem{nil, nil}, err
   143  				}
   144  				data[strings.TrimSuffix(fileInfo.Name(), ".json")] = json.RawMessage(fileData)
   145  
   146  			}
   147  		}
   148  
   149  	}
   150  	fileSystem.Files = data
   151  	return fileSystem, err
   152  }
   153  
   154  func appendErrors(dataType string, ids []string, data map[string]json.RawMessage, errs []error) []error {
   155  	for _, id := range ids {
   156  		if _, ok := data[id]; !ok {
   157  			errs = append(errs, stored_requests.NotFoundError{
   158  				ID:       id,
   159  				DataType: dataType,
   160  			})
   161  		}
   162  	}
   163  	return errs
   164  }