summaryrefslogtreecommitdiffstats
path: root/db/db.go
blob: c99bbea20709586c38af3dc2b74aeedf484f0939 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// SPDX-License-Identifier: ISC
// Copyright © 2021 siddharth <s@ricketyspace.net>

package db

import (
	"encoding/json"
	"fmt"
	"os"
	"path"
	"sync"

	"ricketyspace.net/fern/file"
)

var dbPath string
var defaultDBPath string

// Contains information about list of media that where already
// download for different feeds.
//
// It's stored on disk as a JSON at `$HOME/.config/fern/db.json
type FernDB struct {
	// For locking concurrent read/write access downloaded.
	mutex *sync.RWMutex
	// Key: feed-id
	// Value: feed-id's entries that were downloaded
	downloaded map[string][]string
}

func init() {
	dbPath = "" // Reset.

	// Construct default dbPath
	h, err := os.UserHomeDir()
	if err != nil {
		return
	}
	defaultDBPath = path.Join(h, ".config", "fern", "db.json")
	dbPath = defaultDBPath
}

// Reads the fern db from disk and unmarshals it into a FernDB
// instance.
//
// Returns a pointer to FernDB on success; nil otherwise. The second
// return value is non-nil on error.
func Open() (*FernDB, error) {
	if len(dbPath) == 0 {
		return nil, fmt.Errorf("FernDB path not set")
	}

	// Check if db exists.
	_, err := os.Stat(dbPath)
	if err != nil {
		// db does not exist yet; create an empty one.
		db := new(FernDB)
		db.mutex = new(sync.RWMutex)
		db.downloaded = make(map[string][]string)
		return db, nil
	}

	// Read db from disk.
	f, err := os.Open(dbPath)
	if err != nil {
		return nil, err
	}
	bs, err := file.Read(f)
	if err != nil {
		return nil, err
	}

	// Unmarshal db into an object.
	db := new(FernDB)
	db.mutex = new(sync.RWMutex)
	err = json.Unmarshal(bs, &db.downloaded)
	if err != nil {
		return nil, err
	}
	return db, nil
}

// Checks if entry exists in feed. Assumes the current go routine
// already has the mutex lock. Meant for use by the Exists and Add
// methods.
func (fdb *FernDB) exists(feed, entry string) bool {
	if _, ok := fdb.downloaded[feed]; !ok {
		return false
	}
	for _, e := range fdb.downloaded[feed] {
		if e == entry {
			return true
		}
	}
	return false
}

// Returns true if an `entry` for `feed` exists in the database; false
// otherwise.
func (fdb *FernDB) Exists(feed, entry string) bool {
	// Acquire read lock.
	fdb.mutex.RLock()
	defer fdb.mutex.RUnlock() // Give up lock before returning.

	return fdb.exists(feed, entry)
}

// Adds `feed` <-> `entry` to the database.
//
// Once a `feed` <-> `entry` is added to the database, fern assumes
// that entry was downloaded and will not try downloading the entry
// again.
func (fdb *FernDB) Add(feed, entry string) {
	// Acquire write lock.
	fdb.mutex.Lock()
	defer fdb.mutex.Unlock() // Give up lock before returning.

	// Check if entry already exist for feed.
	if fdb.exists(feed, entry) {
		return
	}

	// Add entry.
	if _, ok := fdb.downloaded[feed]; !ok {
		fdb.downloaded[feed] = make([]string, 0)
	}
	fdb.downloaded[feed] = append(fdb.downloaded[feed], entry)

}

// Writes FernDB to disk in the JSON format.
//
// Returns nil on success; error otherwise
func (fdb *FernDB) Write() error {
	// Acquire write lock.
	fdb.mutex.Lock()
	defer fdb.mutex.Unlock() // Give up lock before returning.

	if len(dbPath) == 0 {
		return fmt.Errorf("FernDB path not set")
	}

	f, err := os.OpenFile(dbPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		return err
	}
	defer f.Close()

	// Marshal database into json.
	bs, err := json.Marshal(fdb.downloaded)
	if err != nil {
		return err
	}

	// Write to disk.
	_, err = f.Write(bs)
	if err != nil {
		return err
	}
	return nil
}

// Sets DB path to the default path. This function is meant to be used
// by tests.
func resetDBPath() {
	dbPath = defaultDBPath
}