Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal for filesystem interface, sample implementation for record.go #178

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 31 additions & 76 deletions core/record.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package core

import (
"encoding/json"
"errors"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -42,12 +40,13 @@ func (r *Record) Duration() time.Duration {
// LoadRecord loads the record with the given start time. Returns
// ErrRecordNotFound if the record cannot be found.
func (t *Timetrace) LoadRecord(start time.Time) (*Record, error) {
path := t.fs.RecordFilepath(start)
path := t.recordFS.FilepathByTime(start)
return t.loadRecord(path)
}

func (t *Timetrace) LoadBackupRecord(start time.Time) (*Record, error) {
path := t.fs.RecordBackupFilepath(start)
// path := t.fs.RecordBackupFilepath(start)
path := t.recordFS.BackupByTime(start)
return t.loadRecord(path)
}

Expand All @@ -60,54 +59,29 @@ func (t *Timetrace) ListRecords(date time.Time) ([]*Record, error) {
// SaveRecord persists the given record. Returns ErrRecordAlreadyExists if the
// record already exists and saving isn't forced.
func (t *Timetrace) SaveRecord(record Record, force bool) error {
path := t.fs.RecordFilepath(record.Start)
path := t.recordFS.FilepathByTime(record.Start)

if _, err := os.Stat(path); err == nil && !force {
if t.recordFS.Exists(path) {
return ErrRecordAlreadyExists
}

if err := t.fs.EnsureRecordDir(record.Start); err != nil {
if err := t.recordFS.EnsureDir(record.Start); err != nil {
return err
}

file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}

bytes, err := json.MarshalIndent(&record, "", "\t")
if err != nil {
return err
}

_, err = file.Write(bytes)

return err
return t.recordFS.Save(path, &record)
}

// BackupRecord creates a backup of the given record file
func (t *Timetrace) BackupRecord(recordKey time.Time) error {
path := t.fs.RecordFilepath(recordKey)
path := t.recordFS.FilepathByTime(recordKey)
record, err := t.loadRecord(path)
if err != nil {
return err
}
// create a new .bak filepath from the record struct
backupPath := t.fs.RecordBackupFilepath(recordKey)
backupPath := t.recordFS.BackupByTime(recordKey)

backupFile, err := os.OpenFile(backupPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}

bytes, err := json.MarshalIndent(&record, "", "\t")
if err != nil {
return err
}

_, err = backupFile.Write(bytes)

return err
return t.recordFS.Save(backupPath, &record)
}

func (t *Timetrace) RevertRecord(recordKey time.Time) error {
Expand All @@ -116,21 +90,9 @@ func (t *Timetrace) RevertRecord(recordKey time.Time) error {
return err
}

path := t.fs.RecordFilepath(recordKey)
path := t.recordFS.FilepathByTime(recordKey)

file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}

bytes, err := json.MarshalIndent(&record, "", "\t")
if err != nil {
return err
}

_, err = file.Write(bytes)

return err
return t.recordFS.Save(path, &record)
}

// RevertRecordsByProject is a function called if user opts to also revert records when they revert a project.
Expand All @@ -154,7 +116,7 @@ func (t *Timetrace) RevertRecordsByProject(key string) error {
keys = append(keys, key)

// get all record dirs and filepaths in order to load the record for matching the parent key
allRecordDirs, err := t.fs.RecordDirs()
allRecordDirs, err := t.recordFS.Dirs()
if err != nil {
return err
}
Expand Down Expand Up @@ -184,13 +146,12 @@ func (t *Timetrace) RevertRecordsByProject(key string) error {
// DeleteRecord removes the given record. Returns ErrRecordNotFound if the
// project doesn't exist.
func (t *Timetrace) DeleteRecord(record Record) error {
path := t.fs.RecordFilepath(record.Start)
path := t.recordFS.FilepathByTime(record.Start)

if _, err := os.Stat(path); os.IsNotExist(err) {
if _, err := t.fs.Stat(path); os.IsNotExist(err) {
return ErrRecordNotFound
}

return os.Remove(path)
return t.recordFS.Delete(path)
}

func (t *Timetrace) DeleteRecordsByProject(key string) error {
Expand All @@ -215,7 +176,7 @@ func (t *Timetrace) DeleteRecordsByProject(key string) error {
keys = append(keys, key)

// get all record dirs and filepaths in order to load the record for matching the parent key
allRecordDirs, err := t.fs.RecordDirs()
allRecordDirs, err := t.recordFS.Dirs()
if err != nil {
return err
}
Expand Down Expand Up @@ -246,7 +207,7 @@ func (t *Timetrace) DeleteRecordsByProject(key string) error {

// EditRecordManual opens the record file in the preferred or default editor.
func (t *Timetrace) EditRecordManual(recordTime time.Time) error {
path := t.fs.RecordFilepath(recordTime)
path := t.recordFS.FilepathByTime(recordTime)

if _, err := t.loadRecord(path); err != nil {
return err
Expand All @@ -263,7 +224,7 @@ func (t *Timetrace) EditRecordManual(recordTime time.Time) error {

// EditRecord loads the record internally, applies the option values and saves the record
func (t *Timetrace) EditRecord(recordTime time.Time, plus string, minus string) error {
path := t.fs.RecordFilepath(recordTime)
path := t.recordFS.FilepathByTime(recordTime)

record, err := t.loadRecord(path)
if err != nil {
Expand All @@ -286,7 +247,7 @@ func (t *Timetrace) EditRecord(recordTime time.Time, plus string, minus string)
func (t *Timetrace) loadAllRecords(date time.Time) ([]*Record, error) {
dir := t.fs.RecordDirFromDate(date)

recordFilepaths, err := t.fs.RecordFilepaths(dir, func(_, _ string) bool {
recordFilepaths, err := t.recordFS.Filepaths(dir, func(_, _ string) bool {
return true
})
if err != nil {
Expand All @@ -307,9 +268,9 @@ func (t *Timetrace) loadAllRecords(date time.Time) ([]*Record, error) {
}

func (t *Timetrace) loadAllRecordsSortedAscending(date time.Time) ([]*Record, error) {
dir := t.fs.RecordDirFromDate(date)
dir := t.recordFS.DirByDate(date)

recordFilepaths, err := t.fs.RecordFilepaths(dir, func(a, b string) bool {
recordFilepaths, err := t.recordFS.Filepaths(dir, func(a, b string) bool {
timeA, _ := time.Parse(recordLayout, a)
timeB, _ := time.Parse(recordLayout, b)
return timeA.Before(timeB)
Expand All @@ -334,7 +295,7 @@ func (t *Timetrace) loadAllRecordsSortedAscending(date time.Time) ([]*Record, er
// LoadLatestRecord loads the youngest record. This may also be a record from
// another day. If there is no latest record, nil and no error will be returned.
func (t *Timetrace) LoadLatestRecord() (*Record, error) {
latestDirs, err := t.fs.RecordDirs()
latestDirs, err := t.recordFS.Dirs()
if err != nil {
return nil, err
}
Expand All @@ -348,7 +309,7 @@ func (t *Timetrace) LoadLatestRecord() (*Record, error) {
return nil, err
}

latestRecords, err := t.fs.RecordFilepaths(dir, func(a, b string) bool {
latestRecords, err := t.recordFS.Filepaths(dir, func(a, b string) bool {
timeA, _ := time.Parse(recordLayout, a)
timeB, _ := time.Parse(recordLayout, b)
return timeA.Before(timeB)
Expand All @@ -369,9 +330,9 @@ func (t *Timetrace) LoadLatestRecord() (*Record, error) {
// loadOldestRecord returns the oldest record of the given date. If there is no
// oldest record, nil and no error will be returned.
func (t *Timetrace) loadOldestRecord(date time.Time) (*Record, error) {
dir := t.fs.RecordDirFromDate(date)
dir := t.recordFS.DirByDate(date)

oldestRecords, err := t.fs.RecordFilepaths(dir, func(a, b string) bool {
oldestRecords, err := t.recordFS.Filepaths(dir, func(a, b string) bool {
timeA, _ := time.Parse(recordLayout, a)
timeB, _ := time.Parse(recordLayout, b)
return timeA.After(timeB)
Expand All @@ -393,7 +354,7 @@ func (t *Timetrace) loadOldestRecord(date time.Time) (*Record, error) {
// through the filter options.
// !imporant: .bak files will be ignored by this function - only .json files in the directory will be read!
func (t *Timetrace) loadFromRecordDir(recordDir string, filter ...func(*Record) bool) ([]*Record, error) {
filesInfo, err := ioutil.ReadDir(recordDir)
filesInfo, err := t.recordFS.DirInfo(recordDir)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -425,7 +386,7 @@ outer:
// loadBackupsFromRecordDir loads all records for one directory and returns them. The slice can be filtered
// through the filter options.
func (t *Timetrace) loadBackupsFromRecordDir(recordDir string, filter ...func(*Record) bool) ([]*Record, error) {
filesInfo, err := ioutil.ReadDir(recordDir)
filesInfo, err := t.recordFS.DirInfo(recordDir)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -455,9 +416,10 @@ outer:
return foundRecords, nil
}

// loadRecord loads a record based of its file path
func (t *Timetrace) loadRecord(path string) (*Record, error) {
file, err := ioutil.ReadFile(path)
if err != nil {
var record Record
if err := t.recordFS.Load(path, &record); err != nil {
if os.IsNotExist(err) {
if strings.HasSuffix(path, ".bak") {
return nil, ErrBackupRecordNotFound
Expand All @@ -466,13 +428,6 @@ func (t *Timetrace) loadRecord(path string) (*Record, error) {
}
return nil, err
}

var record Record

if err := json.Unmarshal(file, &record); err != nil {
return nil, err
}

return &record, nil
}

Expand Down
67 changes: 54 additions & 13 deletions core/timetrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package core
import (
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"time"
Expand All @@ -29,24 +30,64 @@ type Report struct {

// Filesystem represents a filesystem used for storing and loading resources.
type Filesystem interface {
ProjectFilepath(key string) string
ProjectBackupFilepath(key string) string
ProjectFilepaths() ([]string, error)
ProjectBackupFilepaths() ([]string, error)
RecordFilepath(start time.Time) string
RecordBackupFilepath(start time.Time) string
RecordFilepaths(dir string, less func(a, b string) bool) ([]string, error)
RecordDirs() ([]string, error)
ReportDir() string
RecordDirFromDate(date time.Time) string
EnsureDirectories() error
EnsureRecordDir(date time.Time) error
WriteReport(path string, data []byte) error
ProjectFilepath(key string) string // implemented:
ProjectBackupFilepath(key string) string // implemented:
ProjectFilepaths() ([]string, error) // implemented:
ProjectBackupFilepaths() ([]string, error) // implemented:
RecordFilepath(start time.Time) string // implemented:
RecordBackupFilepath(start time.Time) string // implemented:
RecordFilepaths(dir string, less func(a, b string) bool) ([]string, error) // implemented:
RecordDirs() ([]string, error) // implemented:
ReportDir() string // implemented:
RecordDirFromDate(date time.Time) string // implemented:
WriteReport(path string, data []byte) error // implemented:
EnsureRecordDir(date time.Time) error // implemented:

EnsureDirectories() error // in which interface does this func fit??
Stat(path string) (*os.FileInfo, error) // in which interface does this func fit??
}

type ProjectFS interface {
// CRUD operations to interface with stored projects
Load(key string, v interface{}) error
Save(key string, v interface{}) error
Backup(key string, b []byte) error
Delete(key string) error

Filepath(key string) string
BackupFilepath(key string) string
Filepaths() ([]string, error)
BackupFilepaths() ([]string, error)
}

type RecordFS interface {
// CRUD operations to interface with stored records
Load(key string, v interface{}) error
Save(key string, v interface{}) error
Backup(key string, b []byte) error
Delete(path string) error
Exists(path string) bool

BackupByTime(start time.Time) string
Filepaths(dir string, less func(a, b string) bool) ([]string, error)
FilepathByTime(start time.Time) string
Dirs() ([]string, error)
DirInfo(path string) ([]fs.FileInfo, error)
DirByDate(date time.Time) string
EnsureDir(date time.Time) error
}

type ReportFS interface {
Write(path string, data []byte) error
Dir() string
}

type Timetrace struct {
config *config.Config
fs Filesystem
recordFS RecordFS
projectFS ProjectFS
reportFS ReportFS
formatter *Formatter
}

Expand Down
11 changes: 9 additions & 2 deletions fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/dominikbraun/timetrace/config"
"github.com/spf13/afero"
)

const (
Expand All @@ -28,12 +29,14 @@ const (
)

type Fs struct {
Wrapper afero.Fs
config *config.Config
sanitizer *strings.Replacer
}

func New(config *config.Config) *Fs {
return &Fs{
Wrapper: afero.NewMemMapFs(), // change later
config: config,
sanitizer: strings.NewReplacer("/", "-", "\\", "-"),
}
Expand All @@ -58,7 +61,7 @@ func (fs *Fs) ProjectBackupFilepath(key string) string {
func (fs *Fs) ProjectFilepaths() ([]string, error) {
dir := fs.projectsDir()

items, err := ioutil.ReadDir(dir)
items, err := afero.ReadDir(fs.Wrapper, dir)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -212,7 +215,7 @@ func (fs *Fs) EnsureDirectories() error {
}

for _, dir := range dirs {
if err := os.MkdirAll(dir, 0777); err != nil {
if err := fs.Wrapper.MkdirAll(dir, 0777); err != nil {
return err
}
}
Expand Down Expand Up @@ -272,3 +275,7 @@ func (fs *Fs) WriteReport(filepath string, data []byte) error {
}
return nil
}

func (fs *Fs) Stat(path string) (*os.FileInfo, error) {
return nil, fmt.Errorf("not implemented")
}
Loading