package file import ( "errors" "fmt" "io/fs" "os" "path/filepath" "strings" "sync" "git.andreafazzi.eu/andrea/probo/pkg/store" ) type IndexDirFunc[T FileStorable, K store.Storer[T]] func(s *FileStore[T, K]) error var ( ErrorMetaHeaderIsNotPresent = errors.New("Meta header was not found in file.") ) type FileStorable interface { store.Storable Marshal() ([]byte, error) Unmarshal([]byte) error } type FileStorer[T FileStorable] interface { // store.Storer[T] Create(T, ...string) (T, error) ReadAll() []T Read(string) (T, error) Update(T, string) (T, error) Delete(string) (T, error) } type FilePathConfig struct { Dir string FilePrefix string FileSuffix string } type FileStoreConfig[T FileStorable, K store.Storer[T]] struct { FilePathConfig IndexDirFunc func(*FileStore[T, K]) error CreateEntityFunc func() T NoIndexOnCreate bool } type FileStore[T FileStorable, K store.Storer[T]] struct { *FileStoreConfig[T, K] Storer K lock sync.RWMutex paths map[string]string } func DefaultIndexDirFunc[T FileStorable, K store.Storer[T]](s *FileStore[T, K]) error { if s.CreateEntityFunc == nil { return errors.New("CreateEntityFunc cannot be nil!") } files, err := os.ReadDir(s.Dir) if err != nil { return err } entityFiles := make([]fs.DirEntry, 0) for _, file := range files { filename := file.Name() if !file.IsDir() && strings.HasSuffix(filename, s.FileSuffix) { entityFiles = append(entityFiles, file) } } for _, file := range entityFiles { filename := file.Name() fullPath := filepath.Join(s.Dir, filename) fileInfo, err := os.Stat(fullPath) if err != nil { panic(err) } if fileInfo.Size() > 0 { content, err := os.ReadFile(fullPath) if err != nil { return err } entity := s.CreateEntityFunc() err = entity.Unmarshal(content) if err != nil { return fmt.Errorf("An error occurred unmarshalling %v: %v", filename, err) } mEntity, err := s.Create(entity, fullPath) if err != nil { return err } s.SetPath(mEntity, fullPath) } } return nil } func NewFileStore[T FileStorable, K store.Storer[T]](config *FileStoreConfig[T, K], storer K) (*FileStore[T, K], error) { store := &FileStore[T, K]{ FileStoreConfig: config, Storer: storer, paths: make(map[string]string, 0), } if !config.NoIndexOnCreate { err := store.IndexDir() if err != nil { return nil, err } } return store, nil } func (s *FileStore[T, K]) Create(entity T, path ...string) (T, error) { e, err := s.Storer.Create(entity) if err != nil { return e, err } data, err := e.Marshal() if err != nil { return e, err } if len(path) == 0 { path = append(path, filepath.Join(s.Dir, fmt.Sprintf("%s_%v%s", s.FilePrefix, e.GetID(), s.FileSuffix))) } file, err := os.Create(path[0]) if err != nil { return e, err } defer file.Close() _, err = file.Write(data) if err != nil { return e, err } s.SetPath(e, path[0]) return e, nil } func (s *FileStore[T, K]) Update(entity T, id string) (T, error) { e, err := s.Storer.Update(entity, id) if err != nil { return e, err } filePath := s.GetPath(e) data, err := e.Marshal() if err != nil { return e, err } file, err := os.Create(filePath) if err != nil { return e, err } defer file.Close() _, err = file.Write(data) if err != nil { return e, err } return e, nil } func (s *FileStore[T, K]) Read(id string) (T, error) { return s.Storer.Read(id) } func (s *FileStore[T, K]) ReadAll() []T { return s.Storer.ReadAll() } func (s *FileStore[T, K]) Delete(id string) (T, error) { e, err := s.Storer.Delete(id) if err != nil { return e, err } err = os.Remove(s.GetPath(e)) if err != nil { return e, err } return e, nil } func (s *FileStore[T, K]) IndexDir() error { return s.IndexDirFunc(s) } func (s *FileStore[T, K]) GetPath(entity T) string { s.lock.RLock() defer s.lock.RUnlock() return s.paths[entity.GetID()] } func (s *FileStore[T, K]) SetPath(entity T, path string) string { s.lock.Lock() defer s.lock.Unlock() s.paths[entity.GetID()] = path return path }