In the previous blog I have discussed about how to make a key-value storage with a in memory storage. Now I am going to discuss about how you can extend this to use a file system storage.
Let’s first define our structure which is going to hold the information about the storage –
type DiskFS struct {
FS filesystem.Fs
RootFolderName string
}
Now for this project we are going to create our own filesystem implementation we can use afero but I decided to use my own implementation for more learning.
First create a directory in the root folder called filesystem
and create a file called fs.go
and write the following code
package filesystem
import (
"io"
"os"
)
type FileSystem struct {
Fs
}
type File interface {
io.Closer
io.Reader
io.ReaderAt
io.Seeker
io.Writer
io.WriterAt
Name() string
Readdir(count int) ([]os.FileInfo, error)
Stat() (os.FileInfo, error)
Sync() error
WriteString(s string) (ret int, err error)
}
type Fs interface {
Create(name string) (File, error)
Mkdir(name string, perm os.FileMode) error
Open(name string) (File, error)
OpenFile(name string, flag int, perm os.FileMode) (File, error)
Stat(name string) (os.FileInfo, error)
Remove(name string) error
}
Now as we are going to use os module to implement the filesystem create a file called osfs.go
and write the below code
package filesystem
import (
"os"
)
type OsFs struct {
Fs
}
// Return a File System for OS
func NewOsFs() Fs {
return &OsFs{}
}
func (OsFs) Create(name string) (File, error) {
file, err := os.Create(name)
if err != nil {
return nil, err
}
return file, nil
}
func (OsFs) Open(name string) (File, error) {
file, err := os.Open(name)
if err != nil {
return nil, err
}
return file, err
}
func (OsFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
file, err := os.OpenFile(name, flag, perm)
if err != nil {
return nil, err
}
return file, nil
}
func (OsFs) Mkdir(name string, perm os.FileMode) error {
return os.Mkdir(name, perm)
}
func (OsFs) Stat(name string) (os.FileInfo, error) {
return os.Stat(name)
}
func (OsFs) Remove(name string) error {
return os.Remove(name)
}
Now let’s create utility functions to handle some situations. create a utils.go
file and write the code
package filesystem
import (
"os"
)
func DirExists(fs Fs, name string) (bool, error) {
file, err := fs.Stat(name)
if err == nil && file.IsDir() {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func Exists(fs Fs, name string) (bool, error) {
_, err := fs.Stat(name)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func ReadDir(fs Fs, dirName string) ([]os.FileInfo, error) {
dir, err := fs.Open(dirName)
if err != nil {
return nil, err
}
defer dir.Close()
list, err := dir.Readdir(-1)
if err != nil {
return nil, err
}
return list, nil
}
func ReadFile(fs Fs, name string) ([]byte, error) {
file, err := fs.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
data, err := os.ReadFile(name)
if err != nil {
return nil, err
}
return data, nil
}
Ok so our filesystem is done. Now it’s time to write the code for file system based storage implementation. Write the below code inside records.go
file
// Return the Disk structure file system
func NewDisk(rootFolder string) *DiskFS {
diskFs := filesystem.NewOsFs()
ok, err := filesystem.DirExists(diskFs, rootFolder)
if err != nil {
log.Fatalf("Dir exists: %v", err)
}
if !ok {
err := diskFs.Mkdir(rootFolder, os.ModePerm)
if err != nil {
log.Fatalf("Create dir: %v", err)
}
}
return &DiskFS{FS: diskFs, RootFolderName: rootFolder}
}
// Store key, value in the file system
func (d *DiskFS) Store(key, val string) {
file, err := d.FS.Create(d.RootFolderName + "/" + key)
if err != nil {
log.Fatalf("Create file: %v", err)
}
defer file.Close()
_, err = file.Write([]byte(val))
if err != nil {
log.Fatalf("Writing file: %v", err)
}
}
func (d *DiskFS) List() map[string]string {
m := make(map[string]string, 2)
dir, err := filesystem.ReadDir(d.FS, d.RootFolderName)
if err != nil {
log.Fatalf("Error reading the directory: %v", err)
}
for _, fileName := range dir {
content, err := filesystem.ReadFile(d.FS, d.RootFolderName+"/"+fileName.Name())
if err != nil {
log.Fatalf("Error reading the file: %v", err)
}
m[fileName.Name()] = string(content)
}
return m
}
func (d *DiskFS) Get(key string) (string, error) {
ok, err := filesystem.Exists(d.FS, d.RootFolderName+"/"+key)
if err != nil {
log.Fatalf("File exist: %v", err)
}
if ok {
file, err := filesystem.ReadFile(d.FS, d.RootFolderName+"/"+key)
if err != nil {
log.Fatalf("Error reading the file: %v", err)
}
return string(file), nil
}
return "", errors.New("key not found")
}
func (d *DiskFS) Delete(key string) error {
ok, err := filesystem.Exists(d.FS, d.RootFolderName+"/"+key)
if err != nil {
log.Fatalf("File exist: %v", err)
}
if ok {
err = d.FS.Remove(d.RootFolderName + "/" + key)
if err != nil {
log.Fatalf("Delete file err: %v", err)
}
return nil
}
return errors.New("key not found")
}
Now if you run the app with -storage-type=disk
flag you can access all the file system based operation and you can see a directory gets created called storage
and inside it the file created and in the file the content is the value.
In the next part I am going to write the tests for the application.