From 1cb169318c0b25354d9e1bad7ad3b0bcc16c2f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Wed, 19 Jan 2022 01:03:24 +0100 Subject: [PATCH] Add fs filestore --- config.go | 15 +++++- config_test.go | 23 ++++++--- filestore.go | 10 ++-- filestore_fs.go | 115 +++++++++++++++++++++++++++++++++++++++++++ filestore_fs_test.go | 17 +++++++ 5 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 filestore_fs.go create mode 100644 filestore_fs_test.go diff --git a/config.go b/config.go index 2992bfa..e685261 100644 --- a/config.go +++ b/config.go @@ -17,13 +17,20 @@ type ServerConfig struct { } type ServerStoreConfig struct { - Type string `toml:"Type"` + Type string `toml:"Type"` + FS *ServerStoreFSStoreConfig `toml:"FS"` +} + +type ServerStoreFSStoreConfig struct { + Dir string `toml:"Dir"` } func ServerConfigFromReader(r io.Reader) (*ServerConfig, error) { decoder := toml.NewDecoder(r) c := ServerConfig{ - Store: &ServerStoreConfig{}, + Store: &ServerStoreConfig{ + FS: &ServerStoreFSStoreConfig{}, + }, } if err := decoder.Decode(&c); err != nil { return nil, fmt.Errorf("error decoding server config: %w", err) @@ -50,4 +57,8 @@ func (sc *ServerConfig) updateFromEnv() { if value, ok := os.LookupEnv("GPASTE_STORE_TYPE"); ok { sc.Store.Type = value } + + if value, ok := os.LookupEnv("GPASTE_STORE_FS_DIR"); ok { + sc.Store.FS.Dir = value + } } diff --git a/config_test.go b/config_test.go index 20968f7..4d18e20 100644 --- a/config_test.go +++ b/config_test.go @@ -18,14 +18,19 @@ URL = "http://paste.example.org" ListenAddr = ":8080" [Store] -Type = "memory" +Type = "fs" +[Store.FS] +Dir = "/tmp" ` expected := &gpaste.ServerConfig{ LogLevel: "INFO", URL: "http://paste.example.org", ListenAddr: ":8080", Store: &gpaste.ServerStoreConfig{ - Type: "memory", + Type: "fs", + FS: &gpaste.ServerStoreFSStoreConfig{ + Dir: "/tmp", + }, }, } sr := strings.NewReader(simpleConfig) @@ -43,17 +48,21 @@ Type = "memory" clearEnv() var envMap map[string]string = map[string]string{ - "GPASTE_LOGLEVEL": "DEBUG", - "GPASTE_URL": "http://gpaste.example.org", - "GPASTE_STORE_TYPE": "memory", - "GPASTE_LISTENADDR": ":8000", + "GPASTE_LOGLEVEL": "DEBUG", + "GPASTE_URL": "http://gpaste.example.org", + "GPASTE_STORE_TYPE": "fs", + "GPASTE_LISTENADDR": ":8000", + "GPASTE_STORE_FS_DIR": "/tmp", } expected := &gpaste.ServerConfig{ LogLevel: "DEBUG", URL: "http://gpaste.example.org", ListenAddr: ":8000", Store: &gpaste.ServerStoreConfig{ - Type: "memory", + Type: "fs", + FS: &gpaste.ServerStoreFSStoreConfig{ + Dir: "/tmp", + }, }, } diff --git a/filestore.go b/filestore.go index 21cad4f..c3843bf 100644 --- a/filestore.go +++ b/filestore.go @@ -6,12 +6,12 @@ import ( ) type File struct { - ID string - OriginalFilename string - Body io.ReadCloser + ID string `json:"id"` + OriginalFilename string `json:"original_filename"` + MaxViews uint `json:"max_views"` + ExpiresOn time.Time `json:"expires_on"` - MaxViews uint - ExpiresOn time.Time + Body io.ReadCloser } type FileStore interface { diff --git a/filestore_fs.go b/filestore_fs.go new file mode 100644 index 0000000..da71566 --- /dev/null +++ b/filestore_fs.go @@ -0,0 +1,115 @@ +package gpaste + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" +) + +type FSFileStore struct { + dir string + metadata map[string]*File +} + +func NewFSFileStore(dir string) (*FSFileStore, error) { + s := &FSFileStore{ + dir: dir, + metadata: make(map[string]*File), + } + + err := s.readMetadata() + + return s, err +} +func (s *FSFileStore) Store(f *File) error { + defer f.Body.Close() + + metadata := &File{ + ID: f.ID, + OriginalFilename: f.OriginalFilename, + MaxViews: f.MaxViews, + ExpiresOn: f.ExpiresOn, + } + + path := filepath.Join(s.dir, f.ID) + dst, err := os.Create(path) + if err != nil { + return err + } + defer dst.Close() + + if _, err := io.Copy(dst, f.Body); err != nil { + return err + } + s.metadata[f.ID] = metadata + if err := s.writeMetadata(); err != nil { + delete(s.metadata, f.ID) + return err + } + return nil +} + +func (s *FSFileStore) Get(id string) (*File, error) { + metadata, ok := s.metadata[id] + if !ok { + return nil, fmt.Errorf("no such item") + } + + path := filepath.Join(s.dir, id) + f, err := os.Open(path) + if err != nil { + return nil, err + } + metadata.Body = f + return metadata, nil +} + +func (s *FSFileStore) Delete(id string) error { + path := filepath.Join(s.dir, id) + if err := os.Remove(path); err != nil { + return err + } + delete(s.metadata, id) + return nil +} + +func (s *FSFileStore) List() ([]string, error) { + var results []string + for k := range s.metadata { + results = append(results, k) + } + return results, nil +} + +func (s *FSFileStore) writeMetadata() error { + path := filepath.Join(s.dir, "metadata.json") + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + encoder := json.NewEncoder(f) + if err := encoder.Encode(s.metadata); err != nil { + return err + } + return nil +} + +func (s *FSFileStore) readMetadata() error { + path := filepath.Join(s.dir, "metadata.json") + f, err := os.Open(path) + if err != nil { + // TODO: Handle errors better + return nil + } + defer f.Close() + + decoder := json.NewDecoder(f) + if err := decoder.Decode(&s.metadata); err != nil { + return err + } + return nil +} diff --git a/filestore_fs_test.go b/filestore_fs_test.go new file mode 100644 index 0000000..8e1fc81 --- /dev/null +++ b/filestore_fs_test.go @@ -0,0 +1,17 @@ +package gpaste_test + +import ( + "testing" + + "git.t-juice.club/torjus/gpaste" +) + +func TestFSFileStore(t *testing.T) { + dir := t.TempDir() + s, err := gpaste.NewFSFileStore(dir) + if err != nil { + t.Fatalf("Error creating store: %s", err) + } + + RunFilestoreTest(s, t) +}