-
Notifications
You must be signed in to change notification settings - Fork 0
/
store.go
239 lines (194 loc) · 8.38 KB
/
store.go
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package main
import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
const defaultRootFolderName = "nimus_root"
// --------------------------Path Transform Functions------------------------------------ //
func CASPathTransformFunc(key string) PathKey {
// Hash the key using SHA-1
hash := sha1.Sum([]byte(key)) // sha1 gives out a hash of 20 bytes of the key. Lets say the key is "How are we doing?" then the hash will be "6804429f74181a63c50c3d81d733a12f14a353ff" of length 40
hashStr := hex.EncodeToString(hash[:]) // we had to do hex encoding to convert the hash to a string as the hash is a byte array so first the hex encoding is done and then the string conversion is done
// Let's say the hash is 6804429f74181a63c50c3d81d733a12f14a353ff so with the blocksize of 5 we will get the following paths : 68044/29f74/181a6/3c50c/3d81d/733a1/2f14a/353ff
blocksize := 5
sliceLen := len(hashStr) / blocksize
paths := make([]string, sliceLen)
for i := 0; i < sliceLen; i++ {
from, to := i*blocksize, (i*blocksize)+blocksize
paths[i] = hashStr[from:to]
}
return PathKey{
PathName: strings.Join(paths, "/"), // Concatenate hash parts to form a path
Filename: hashStr, // The full hash as the filename
}
}
type PathTransformFunc func(string) PathKey
var DefaultPathTransformFunc = func(key string) PathKey {
return PathKey{
PathName: key, // Default path transformation just uses the key
Filename: key,
}
} // DefaultPathTransformFunc will be used if the user does not provide any path transformation function
// ------------------------------ XXXXXXXXXXXXXXX---------------------------------------- //
// ------------------------------- PathKey Struct --------------------------------------- //
type PathKey struct {
PathName string // The path formed from the hashed key
Filename string // The full hash used as the filename
} // PathKey might look like if user provided key as "How you doin'?" the filename will be "6804429f74181a63c50c3d81d733a12f14a353ff" and the path will be "68044/29f74/181a6/3c50c/3d81d/733a1/2f14a/353ff
func (p PathKey) FirstPathName() string {
// Retrieve the first part of the path
paths := strings.Split(p.PathName, "/")
if len(paths) == 0 {
return ""
}
return paths[0]
}
func (p PathKey) FullPath() string {
// Construct the full path by combining PathName and Filename
return fmt.Sprintf("%s/%s", p.PathName, p.Filename)
} // FullPath then will be "68044/29f74/181a6/3c50c/3d81d/733a1/2f14a/353ff/6804429f74181a63c50c3d81d733a12f14a353ff"
// ------------------------------ XXXXXXXXXXXXXXX---------------------------------------- //
//------------------------ Store Options and Constructor -------------------------------- //
type Store struct {
StoreOpts // Embedding StoreOpts to use its fields directly
}
type StoreOpts struct {
Root string // Root folder for the storage system
PathTransformFunc PathTransformFunc // Function to transform keys into paths
}
func NewStore(opts StoreOpts) *Store {
// Initialize default path transform function if not provided
if opts.PathTransformFunc == nil {
opts.PathTransformFunc = DefaultPathTransformFunc
}
// Set default root if not provided
if opts.Root == "" {
opts.Root = defaultRootFolderName
}
return &Store{
StoreOpts: opts,
}
}
// Let's say the root is "nimus_dir" and the path transform function is CASPathTransformFunc then the store will be created with these options
// and for eg. if the user provides : key = 'hello.txt' id = 'user1' then the path will be: nimus_dir/user1/68044/29f74/181a6/3c50c/3d81d/733a1/2f14a/353ff/hello.txt
// ------------------------------ XXXXXXXXXXXXXXX---------------------------------------- //
// --------------------------------Store Methods ---------------------------------------- //
/* 1. Write the file to the store and return the number of bytes written ---------------- */
func (s *Store) Write(id string, key string, r io.Reader) (int64, error) {
// Write data to a file in the store
return s.writeStream(id, key, r)
}
func (s *Store) writeStream(id string, key string, r io.Reader) (int64, error) {
// Write data from the reader to the file
f, err := s.openFileForWriting(id, key)
if err != nil {
return 0, err
}
return io.Copy(f, r)
}
func (s *Store) openFileForWriting(id string, key string) (*os.File, error) {
// Open a file for writing, creating directories as needed
pathKey := s.PathTransformFunc(key)
pathNameWithRoot := fmt.Sprintf("%s/%s/%s", s.Root, id, pathKey.PathName) // The pathNameWithRoot will be "nimus_dir/user1/68044/29f74/181a6/3c50c/3d81d/733a1/2f14a/353ff"
if err := os.MkdirAll(pathNameWithRoot, os.ModePerm); err != nil { // os.MkdirAll creates a directory named path, along with any necessary parents, and returns nil, or else returns an error.
return nil, err
}
fullPathWithRoot := fmt.Sprintf("%s/%s/%s", s.Root, id, pathKey.FullPath()) // The fullPathWithRoot will be "nimus_dir/user1/68044/29f74/181a6/3c50c/3d81d/733a1/2f14a/353ff/hello.txt"
f, err := os.Create(fullPathWithRoot) // os.Create creates or truncates the named file. If the file already exists, it is truncated. If the file does not exist, it is created with mode 0666 (before umask).
if err != nil {
return nil, err
}
return f, nil // The file will be created and returned
}
/* 2. Read the file from the store and return the number of bytes read and the reader --- */
func (s *Store) Read(id string, key string) (int64, io.Reader, error) {
// Read data from a file in the store
return s.readStream(id, key)
}
func (s *Store) readStream(id string, key string) (int64, io.ReadCloser, error) {
// Read data from the file and return the size and reader
pathKey := s.PathTransformFunc(key)
fullPathWithRoot := fmt.Sprintf("%s/%s/%s", s.Root, id, pathKey.FullPath())
file, err := os.Open(fullPathWithRoot)
if err != nil {
return 0, nil, err
}
fileStat, err := file.Stat()
if err != nil {
return 0, nil, err
}
sizeOfFile := fileStat.Size()
return sizeOfFile, file, nil
}
/* 3. Check if the file exists in the store ---------------------------------------------- */
func (s *Store) Has(id string, key string) bool {
// Check if a file exists in the store
pathKey := s.PathTransformFunc(key)
fullPathWithRoot := fmt.Sprintf("%s/%s/%s", s.Root, id, pathKey.FullPath())
_, err := os.Stat(fullPathWithRoot) // os.Stat returns file info. It will return an error if the file does not exist
return !errors.Is(err, os.ErrNotExist) // errors.Is reports whether any error in err's chain matches target. os.ErrNotExist is the error returned by os.Stat when the file does not exist
}
/* 4. Delete the file from the store ----------------------------------------------------- */
func (s *Store) Delete(id string, key string) error {
pathKey := s.PathTransformFunc(key)
fullPathWithRoot := fmt.Sprintf("%s/%s/%s", s.Root, id, pathKey.FullPath())
// Remove the specific file
if err := os.Remove(fullPathWithRoot); err != nil {
return fmt.Errorf("failed to delete file: %w", err)
}
// Clean up empty directories up to the root
for {
parentDir := filepath.Dir(fullPathWithRoot)
if parentDir == s.Root {
break // Stop at the root folder to avoid unintended deletions
}
isEmpty, err := isDirEmpty(parentDir)
if err != nil {
return fmt.Errorf("failed to check directory: %w", err)
}
if isEmpty {
if err := os.Remove(parentDir); err != nil {
return fmt.Errorf("failed to remove directory: %w", err)
}
} else {
break // Stop if we encounter a non-empty directory
}
// Move up to the parent directory
fullPathWithRoot = parentDir
}
return nil
}
// to check if the dir is empty first
func isDirEmpty(dir string) (bool, error) {
f, err := os.Open(dir)
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdir(1)
if err == io.EOF {
return true, nil
}
return false, err
}
// for testing purposes to clear away the entire storage
func (s *Store) Clear() error {
// Clear the entire storage by removing the root directory
return os.RemoveAll(s.Root)
}
/* 5. Write the file to the store and return the number of bytes written ---------------- */
func (s *Store) WriteDecrypt(encKey []byte, id string, key string, r io.Reader) (int64, error) {
f, err := s.openFileForWriting(id, key)
if err != nil {
return 0, err
}
n, err := copyDecrypt(encKey, r, f)
return int64(n), err
}
// ------------------------------ XXXXXXXXXXXXXXX----------------------------------------- //