Files
golink/db.go
T
Will Norris 41c8dbe5b1 cmd/golink: refactor storage logic into db.go
This is a straight refactor with no functional changes. All file access
is captured in the FileDB type. The DB interface is not strictly
necessary here since we only have a single implementation, but helped to
keep the API clean, which will be useful in a future sqlite migration.

Change-Id: I9b86db3040e2618a4ffef237369288ccfc10bc1e
2022-06-10 10:11:11 -07:00

106 lines
2.5 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
// Link is the structure stored for each go short link.
type Link struct {
Short string // the "foo" part of http://go/foo
Long string // the target URL or text/template pattern to run
Created time.Time
LastEdit time.Time // when the link was last edited
Owner string // user@domain
}
// DB provides storage for Links.
type DB interface {
// List the short name of all stored Links.
List() ([]string, error)
// Load a Link by its short name. It returns fs.ErrNotExist if the link does not exist.
Load(short string) (*Link, error)
// Save a Link.
Save(link *Link) error
}
// FileDB stores Links in JSON files on disk.
type FileDB struct {
// dir is the directory to store one JSON file per link.
dir string
}
// NewFileDB returns a new FileDB which will store links in individual JSON
// files in the specified directory. If mkdir is true, the directory will be
// created if it does not exist.
func NewFileDB(dir string, mkdir bool) (*FileDB, error) {
if mkdir {
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, err
}
}
if fi, err := os.Stat(dir); err != nil {
return nil, err
} else if !fi.IsDir() {
return nil, fmt.Errorf("%q is not a directory", dir)
}
return &FileDB{dir: dir}, nil
}
// linkPath returns the path to the file on disk for the specified link. Short
// name is normalized to be case insensitive, remove dashes, and escape some
// characters.
//
// TODO(willnorris): some of this normalization is not unique to FileDB and
// should be moved elsewhere
func (f *FileDB) linkPath(short string) string {
name := url.PathEscape(strings.ToLower(short))
name = strings.ReplaceAll(name, "-", "")
name = strings.ReplaceAll(name, ".", "%2e")
return filepath.Join(f.dir, name)
}
func (f *FileDB) List() ([]string, error) {
d, err := os.Open(f.dir)
if err != nil {
return nil, err
}
defer d.Close()
names, err := d.Readdirnames(0)
if err != nil {
return nil, err
}
return names, nil
}
func (f *FileDB) Load(short string) (*Link, error) {
data, err := os.ReadFile(f.linkPath(short))
if err != nil {
return nil, err
}
link := new(Link)
if err := json.Unmarshal(data, link); err != nil {
return nil, err
}
return link, nil
}
func (f *FileDB) Save(link *Link) error {
j, err := json.MarshalIndent(link, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(f.linkPath(link.Short), j, 0600); err != nil {
return err
}
return nil
}