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 }