tme

Toggl like Time Manager
git clone git://gtms.dev/tme
Log | Files | Refs

repository.go (5633B)


      1 package main
      2 
      3 import (
      4 	"errors"
      5 	"fmt"
      6 	"io"
      7 	"io/fs"
      8 	"os"
      9 	"path"
     10 	"path/filepath"
     11 	"strings"
     12 	"time"
     13 )
     14 
     15 type FSRepository struct {
     16 	rootPath string
     17 }
     18 
     19 func NewFSRepository(rootPath string) FSRepository {
     20 	return FSRepository{rootPath}
     21 }
     22 
     23 func (repo FSRepository) RunningEntry(group Group, entryTime *TimeContext) (Entry, error) {
     24 	entryPath := path.Join(repo.rootPath, group.Name, activeFile)
     25 	if _, err := os.Stat(entryPath); os.IsNotExist(err) {
     26 		return Entry{}, err
     27 	}
     28 
     29 	data, err := os.ReadFile(entryPath)
     30 	if err != nil {
     31 		return Entry{}, err
     32 	}
     33 
     34 	firstLine := strings.Split(string(data), "\n")[0]
     35 	start, _ := entryTime.ParseEntry(firstLine)
     36 
     37 	timeRange, err := NewTimeRange(start, time.Now())
     38 	if err != nil {
     39 		return Entry{}, err
     40 	}
     41 
     42 	return Entry{
     43 		TimeRange: timeRange,
     44 		Completed: false,
     45 	}, nil
     46 }
     47 
     48 func (r FSRepository) Save(group Group, entry Entry) error {
     49 	// TODO(tms) 22.10.22: maybe ask if user want create new folder (fe: typo)
     50 	groupPath := path.Join(r.rootPath, group.Name)
     51 	if err := os.MkdirAll(groupPath, os.ModePerm); err != nil {
     52 		return err
     53 	}
     54 
     55 	entryPath := path.Join(r.rootPath, group.Name, r.EntryName(entry))
     56 	if err := os.WriteFile(entryPath, []byte(r.EntryData(entry)), os.ModePerm); err != nil {
     57 		return err
     58 	}
     59 
     60 	return nil
     61 }
     62 
     63 func (repo FSRepository) EntryData(entry Entry) string {
     64 	if entry.Completed {
     65 		return fmt.Sprintf("%s\n%s\n", entry.TimeRange.Start.Format(DataTimeLayout), entry.TimeRange.Stop.Format(DataTimeLayout))
     66 	}
     67 
     68 	return fmt.Sprintf("%s\n", entry.TimeRange.Start.Format(DataTimeLayout))
     69 }
     70 
     71 func (r FSRepository) EntryName(entry Entry) string {
     72 	if entry.Completed {
     73 		return strings.Join([]string{entry.TimeRange.Start.Format(FileNameLayout), entry.TimeRange.Stop.Format(FileNameLayout)}, "_")
     74 	}
     75 
     76 	return activeFile
     77 }
     78 
     79 func (repo FSRepository) Remove(group Group, entry Entry) error {
     80 	entryPath := path.Join(repo.rootPath, group.Name, repo.EntryName(entry))
     81 	err := os.Remove(entryPath)
     82 	if err != nil {
     83 		return err
     84 	}
     85 
     86 	groupPath := path.Join(repo.rootPath, group.Name)
     87 	if empty, _ := repo.IsEmpty(groupPath); empty {
     88 		err = os.Remove(groupPath)
     89 		if err != nil {
     90 			return err
     91 		}
     92 	}
     93 
     94 	return nil
     95 }
     96 
     97 func (repo FSRepository) IsEmpty(groupPath string) (bool, error) {
     98 	f, err := os.Open(groupPath)
     99 	if err != nil {
    100 		return false, err
    101 	}
    102 	defer f.Close()
    103 
    104 	_, err = f.Readdirnames(1) // Or f.Readdir(1)
    105 	if err == io.EOF {
    106 		return true, nil
    107 	}
    108 
    109 	return false, err // Either not empty or error, suits both cases
    110 }
    111 
    112 func (repo FSRepository) ExistsEntry(group Group, entry Entry) bool {
    113 	fullPath := path.Join(repo.rootPath, group.Name, repo.EntryName(entry))
    114 	if _, err := os.Stat(fullPath); !os.IsNotExist(err) {
    115 		return true
    116 	}
    117 	return false
    118 }
    119 
    120 func (repo FSRepository) Exists(group Group) bool {
    121 	groupPath := path.Join(repo.rootPath, group.Name)
    122 	if _, err := os.Stat(groupPath); !os.IsNotExist(err) {
    123 		return true
    124 	}
    125 	return false
    126 }
    127 
    128 func (repo FSRepository) ListEntries(group Group, entryTime *TimeContext) ([]Entry, error) {
    129 	if !repo.Exists(group) {
    130 		return []Entry{}, errors.New("Group '" + group.Name + "' does not exist")
    131 	}
    132 
    133 	var entries []Entry
    134 	groupPath := path.Join(repo.rootPath, group.Name)
    135 	err := filepath.WalkDir(groupPath, func(path string, d fs.DirEntry, err error) error {
    136 		if d.IsDir() {
    137 			if path == groupPath {
    138 				return nil
    139 			}
    140 			return fs.SkipDir
    141 		}
    142 
    143 		entry, err := repo.find(path, entryTime)
    144 		if err != nil {
    145 			fmt.Fprintln(os.Stderr, err)
    146 			os.Exit(1)
    147 		}
    148 		entries = append(entries, entry)
    149 		return nil
    150 	})
    151 	if err != nil {
    152 		return []Entry{}, err
    153 	}
    154 
    155 	return entries, nil
    156 }
    157 
    158 func (repo FSRepository) ListGroups(groupPath string) ([]Group, error) {
    159 	var groups []Group
    160 	err := filepath.WalkDir(path.Join(repo.rootPath, groupPath), func(path string, d fs.DirEntry, err error) error {
    161 		if d.IsDir() {
    162 			if path == groupPath {
    163 				return nil
    164 			}
    165 			gp := strings.TrimPrefix(path, repo.rootPath)
    166 			gp = strings.TrimPrefix(gp, string(os.PathSeparator))
    167 			group := NewGroup(gp)
    168 			groups = append(groups, group)
    169 		}
    170 		return nil
    171 	})
    172 	if err != nil {
    173 		return []Group{}, err
    174 	}
    175 
    176 	return groups, nil
    177 }
    178 
    179 func (repo FSRepository) find(entryPath string, timeContext *TimeContext) (Entry, error) {
    180 	if _, err := os.Stat(entryPath); os.IsNotExist(err) {
    181 		return Entry{}, err
    182 	}
    183 
    184 	data, err := os.ReadFile(entryPath)
    185 	if err != nil {
    186 		return Entry{}, err
    187 	}
    188 
    189 	// TODO(tms) 11.11.22: format check
    190 	base := path.Base(entryPath)
    191 
    192 	lines := strings.Split(string(data), "\n")
    193 
    194 	if base == activeFile {
    195 		start, _ := timeContext.ParseEntry(lines[0])
    196 		timeRange, err := NewTimeRange(start, start.Add(time.Second))
    197 		return Entry{Completed: false, TimeRange: timeRange}, err
    198 	} else {
    199 		start, _ := timeContext.ParseEntry(lines[0])
    200 		stop, _ := timeContext.ParseEntry(lines[1])
    201 		timeRange, err := NewTimeRange(start, stop)
    202 		return Entry{Completed: true, TimeRange: timeRange}, err
    203 	}
    204 }
    205 
    206 func (repo FSRepository) Move(from Group, entries []Entry, to Group) (int, error) {
    207 	counter := 0
    208 
    209 	if !repo.Exists(from) {
    210 		return counter, errors.New("Group '" + from.Name + "' does not exist")
    211 	}
    212 
    213 	if !repo.Exists(to) {
    214 		return counter, errors.New("Group '" + from.Name + "' does not exist")
    215 	}
    216 
    217 	for _, entry := range entries {
    218 		entryPath := path.Join(repo.rootPath, from.Name, repo.EntryName(entry))
    219 		if !repo.ExistsEntry(from, entry) {
    220 			return counter, errors.New("Entry '" + entryPath + "' does not exist")
    221 		}
    222 
    223 		newEntryPath := path.Join(repo.rootPath, to.Name, repo.EntryName(entry))
    224 
    225 		err := os.Rename(entryPath, newEntryPath)
    226 		if err != nil {
    227 			return counter, err
    228 		}
    229 
    230 		counter = counter + 1
    231 	}
    232 
    233 	return counter, nil
    234 }