package filesystem import ( "errors" "fmt" "os" "path/filepath" "strings" "code.uint32.ru/tiny/storage/internal/errinternal" ) const ( fileModeDir os.FileMode = 0755 fileModeFile os.FileMode = 0644 ) var ( ErrInvalidKey = errinternal.ErrInvalidKey ErrNotFound = errinternal.ErrNotFound ) func New(path string) (*Storage, error) { info, err := os.Stat(path) if err != nil { return nil, err } if !info.IsDir() { return nil, fmt.Errorf("provided path %s is not a directory", path) } abs, err := filepath.Abs(path) if err != nil { return nil, fmt.Errorf("could not tarnslate %s to absolute path", path) } return &Storage{Dir: abs}, nil } type Storage struct { Dir string } func (s *Storage) Save(key string, data []byte) error { if err := validateKey(key); err != nil { return err } path := s.getKeyPath(key) if err := os.MkdirAll(path, fileModeDir); err != nil { return err } if err := os.WriteFile(filepath.Join(path, key), data, fileModeFile); err != nil { return err } return nil } func (s *Storage) Load(key string) ([]byte, error) { if err := validateKey(key); err != nil { return nil, err } path := s.getKeyPath(key) b, err := os.ReadFile(filepath.Join(path, key)) if err != nil && errors.Is(err, os.ErrNotExist) { return nil, errors.Join(errinternal.ErrNotFound, err) } else if err != nil { return nil, err } return b, nil } func (s *Storage) Delete(key string) error { if err := validateKey(key); err != nil { return err } path := s.getKeyPath(key) err := os.Remove(filepath.Join(path, key)) if err != nil && errors.Is(err, os.ErrNotExist) { return nil } else if err != nil { return err } // TODO: think of cleaning up path when no files left // in /basedir/a/b/ after deleting key abc return nil } func (s *Storage) getKeyPath(key string) string { return filepath.Join(s.Dir, getPrefixPath(key)) } func validateKey(key string) error { if len([]rune(key)) < 3 { return errors.Join(ErrInvalidKey, fmt.Errorf("key must be at least 3 characters long")) } // Of course windoze guys are missing the whole point, but // let us use os-specific path separator and not ruin the whole fun // for them :) if strings.Contains(key, string(os.PathSeparator)) { return errors.Join(ErrInvalidKey, fmt.Errorf("key must not contain path separator character: %s", string(os.PathSeparator))) } return nil } func getPrefixPath(key string) string { r := []rune(key) out := []rune{r[0], os.PathSeparator, r[1]} return string(out) }