-
Notifications
You must be signed in to change notification settings - Fork 0
/
filesystem.go
750 lines (652 loc) · 19 KB
/
filesystem.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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
package filesystem
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
)
// TASKS
///////////////////////////////////////////////////////////////////////////////
// * File locking for reading and writing for atomicity
// Reference:
//
// https://github.com/golang/go/blob/master/src/cmd/go/internal/lockedfile/mutex.go
//
// * Add ability to have error checking raptor/reed-solomon etc read/writes
// * zero-copy or something
// * Missing links, symlinks etc
// * Make a type that is a smart truncate for logs. It will chop off the first
// portion, leaving the remainder and then append to that amount. at a set
// amount of like 1 MB of data.
////////////////////////////////////////////////////////////////////////////////
// NOTE
// The goal is to be a very simple filesystem interface, simplifying interaction
// with the filesystem by abstracting a thin as possible layer making code
// expressive as possible. To be successful this file must stay small and not
// complex at all; but also should make working with filesystems very natural,
// and even have validation for security.
//
// * **So far this model benefits greatly from avoiding holding locks or mem**
// longer than absolutely necessary.
//
// * It features chainable functionality.
//
////////////////////////////////////////////////////////////////////////////////
// NOTE If we can prevent ALL path errors by validating and cleaning input, we
// can have an interface without errors ouputs, or at least a choke-point
// where they would occur; leaving the rest of the API simpler.
// If there is an error, it will be of type *PathError.
type Path string
// TODO: Just maybe these should include the os.File (or not)
type Directory Path
type File Path
type Hash string
type Line string
const (
LineBreak = "\n"
Return = "\r"
)
// TODO: Chown, Chmod, SoftLink, HardLink, Stream Write, Stream Read, Zero-Copy
func ParsePath(path string) Path { return Path(path).Clean() }
func (self Path) String() string { return string(self) }
////////////////////////////////////////////////////////////////////////////////
func (self Path) Directory(directory string) Path {
return Path(fmt.Sprintf("%s/%s/", self.String(), directory))
}
func (self Path) File(filename string) Path {
return Path(fmt.Sprintf("%s/%s", Path(self).String(), filename))
}
///////////////////////////////////////////////////////////////////////////////
func (self Directory) Directory(directory string) Path {
return Path(self).Directory(directory)
}
func (self Directory) String() string { return string(self) }
func (self Directory) Path() Path { return Path(self) }
func (self Directory) Name() string { return filepath.Base(Path(self).String()) }
func (self Directory) File(filename string) (File, error) {
if 0 < len(filename) {
return File(fmt.Sprintf("%s/%s", Path(self).String(), filename)), nil
} else {
if self.Path().IsFile() {
return File(self), nil
} else {
return File(self), fmt.Errorf("error: path does not resolve to file")
}
}
}
func (self Directory) List() (list []Path) {
files, err := ioutil.ReadDir(self.Path().String())
if err != nil {
panic(err)
}
for _, fileInfo := range files {
list = append(list, Path(filepath.Join(self.String(), fileInfo.Name())))
}
return list
}
func (self Directory) Subdirectories() (list []Directory) {
for _, file := range self.List() {
if file.IsDirectory() {
list = append(list, Directory(file))
}
}
return list
}
func (self Directory) Subfiles() (list []File) {
for _, file := range self.List() {
if file.IsFile() {
list = append(list, File(file))
}
}
return list
}
///////////////////////////////////////////////////////////////////////////////
func (self File) Directory() (Directory, error) {
if self.Path().IsDirectory() {
return Directory(self), nil
} else {
return Directory(self), fmt.Errorf("error: path does not resolve to directory")
}
}
func (self File) BaseDirectory() Directory {
if path, err := filepath.Abs(self.String()); err != nil {
panic(err)
} else {
return Directory(filepath.Dir(path))
}
}
func (self File) Path() Path { return Path(self) }
func (self File) Name() string { return filepath.Base(Path(self).String()) }
func (self File) Basename() string { return self.Name()[0:(len(self.Name()) - len(self.Extension()))] }
// TODO: In a more complete solution, we would also use magic sequence and mime;
// but that would have to be segregated into an interdependent submodule or not
// at all.
func (self File) Extension() string { return filepath.Ext(Path(self).String()) }
// BASIC OPERATIONS ///////////////////////////////////////////////////////////
// NOTE: Create directories if they don't exist, or simply create the
// directory, so we can have a single create for either file or directory.
func (self Path) Move(path string) error {
if self.Exists() {
Path(path).Create()
return self.Remove()
} else {
return fmt.Errorf("error: file does not exist")
}
}
func (self Path) Rename(path string) error {
baseDirectory := filepath.Dir(path)
os.MkdirAll(baseDirectory, os.ModePerm)
return os.Rename(self.String(), path)
}
func (self Path) Remove() error { return os.RemoveAll(self.String()) }
// INFO / META ////////////////////////////////////////////////////////////////
// NOTE: Lets always clean before we get to these so no error is possible.
func (self Path) Metadata() os.FileInfo {
info, err := os.Stat(self.String())
if err != nil {
panic(err)
}
return info
}
// TODO: For folders we shuld calculate the size of all the contents recursively
// eventually but for now we just need the file size.
func (self Path) Size() int64 {
return self.Metadata().Size()
}
func (self Path) UID() int {
if stat, ok := self.Metadata().Sys().(*syscall.Stat_t); ok {
return int(stat.Uid)
} else {
panic(fmt.Errorf("error: failed to obtain uid of: ", self.String()))
}
}
func (self Path) GUID() int {
if stat, ok := self.Metadata().Sys().(*syscall.Stat_t); ok {
return int(stat.Gid)
} else {
panic(fmt.Errorf("error: failed to obtain guid of: ", self.String()))
}
}
func (self Path) Permissions() os.FileMode {
return self.Metadata().Mode()
}
// IO /////////////////////////////////////////////////////////////////////////
func (self Path) Create() {
switch {
case self.IsDirectory():
Directory(self).Create()
case self.IsFile():
File(self).Create()
default:
panic(fmt.Errorf("error: unsupported type"))
}
}
func (self Directory) Create() {
if self.Path().IsFile() {
File(self.Path()).Create()
} else {
if !self.Path().Exists() {
err := os.MkdirAll(self.String(), 0700|os.ModeSticky)
if err != nil {
panic(err)
}
}
}
}
func (self File) Create() File {
if self.Path().IsDirectory() {
Directory(self.Path()).Create()
} else {
path, err := filepath.Abs(self.String())
if err != nil {
panic(err)
}
Directory(filepath.Dir(path)).Create()
if !self.Path().Exists() {
file, err := os.OpenFile(self.String(), os.O_CREATE|os.O_WRONLY, 0640|os.ModeSticky)
if err != nil && file.Close() != nil {
panic(err)
}
}
}
return self
}
func (self File) Overwrite() File {
if self.Path().IsDirectory() {
Directory(self.Path()).Create()
} else {
path, err := filepath.Abs(self.String())
if err != nil {
panic(err)
}
Directory(filepath.Dir(path)).Create()
file, err := os.OpenFile(self.String(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640|os.ModeSticky)
if err != nil && file.Close() != nil {
panic(err)
}
}
return self
}
// TODO: Maybe ChangePermissions to match the expected Chmod and changeowner
// chown? Right now its Path -> read, and file and directroy are set. Which is
// not exsactly natural
func (self File) Permissions(permissions os.FileMode) File {
err := os.Chmod(self.String(), permissions)
if err != nil {
panic(err)
}
return self
}
func (self File) Chmod(permissions os.FileMode) File {
return self.Permissions(permissions)
}
func (self File) Owner(username string) File {
u, err := user.Lookup(username)
var uid int
if u != nil {
uid, err = strconv.Atoi(u.Uid)
if err != nil {
panic(err)
}
} else if err != nil {
user, idError := user.LookupId(username)
if idError != nil {
panic(err)
}
uid, err = strconv.Atoi(user.Uid)
if err != nil {
panic(err)
}
}
os.Chown(self.String(), uid, self.Path().GUID())
return self
}
func (self File) Group(guid int) File {
// TODO: Like above, should be checking the group exists
os.Chown(self.String(), self.Path().UID(), guid)
return self
}
func (self File) Chown(uid, guid int) File {
// TODO: Like above, should be checking the group exists
os.Chown(self.String(), uid, guid)
return self
}
// NOTE: In the case of directories, may list contents?
func (self File) Open() *os.File {
if !self.Path().Exists() {
self = self.Create()
}
openedFile, err := os.Open(self.String())
if err != nil {
panic(err)
}
return openedFile
}
func (self File) ReadOnly() *os.File {
if !self.Path().Exists() {
self = self.Create()
}
openedFile, err := os.OpenFile(self.String(), os.O_RDONLY, 0640|os.ModeSticky)
if err != nil {
panic(err)
}
return openedFile
}
func (self File) WriteOnly() *os.File {
if !self.Path().Exists() {
self = self.Create()
}
openedFile, err := os.OpenFile(self.String(), os.O_WRONLY|os.O_APPEND, 0640|os.ModeSticky)
if err != nil {
panic(err)
}
return openedFile
}
func (self File) ReadWrite() *os.File {
if !self.Path().Exists() {
self = self.Create()
}
openedFile, err := os.OpenFile(self.String(), os.O_RDWR|os.O_APPEND, 0640|os.ModeSticky)
if err != nil {
panic(err)
}
return openedFile
}
func (self File) Sync() *os.File {
if !self.Path().Exists() {
self = self.Create()
}
openedFile, err := os.OpenFile(self.String(), os.O_SYNC|os.O_APPEND, 0640|os.ModeSticky)
if err != nil {
panic(err)
}
return openedFile
}
func (self File) Fd() uintptr { return self.ReadOnly().Fd() }
// TODO: In the future; pass in Byte, Kilobyte, Megabyte, etc
func (self File) Size() int64 { return self.Path().Size() }
// IO: Reads //////////////////////////////////////////////////////////////////
// TODO: Would like the ability to read lines, last 20, first 20, or specific
// line. Then we can do specific line edits with this library, patches, diffs,
// etc.
// NOTE: Simply read ALL bytes
func (self File) Bytes() (output []byte) {
if self.Path().Exists() {
output, err := ioutil.ReadFile(self.String())
if err != nil {
// TODO: For now we will panic on errors so we can catch any that slip
// by and squash them or move them downstream to the validation/cleanup
// chokepoint.
panic(err)
}
return output
} else {
panic(fmt.Errorf("error: no file exists"))
}
}
func (self File) String() string { return string(self.Bytes()) }
// NOTE: This is essentially a Head as is
func (self File) HeadBytes(readSize int) ([]byte, error) {
var limitBytes []byte
file := self.Open()
// TODO: Use seek, seek is really powerful, it lets you jump forward back,
// etc.
readBytes, err := io.ReadAtLeast(file, limitBytes, readSize)
if readBytes != readSize {
return limitBytes, fmt.Errorf("error: failed to complete read: read ", readBytes, " out of ", readSize, "bytes")
} else {
return limitBytes, err
}
}
// NOTE: This is essentially a Head as is
func (self File) TailBytes(readSize int) ([]byte, error) {
var data []byte
file := self.Open()
bytesRead, err := io.ReadAtLeast(file, data, readSize)
if bytesRead != readSize {
return data, fmt.Errorf("error: failed to complete read: read ", bytesRead, " out of ", readSize, "bytes")
} else {
return data, err
}
}
// LINES //////////////////////////////////////////////////////////////////////
// TODO: Add all the code needed for easily abstracting patching, diffs, and
// similar functionality using this library as the backend.
func (self File) Lines() []Line {
var lines []Line
for _, line := range strings.Split(self.String(), LineBreak) {
lines = append(lines, Line(line))
}
return lines
}
func (self File) HeadLines(lineCount int) []Line {
var lines []Line
for index, line := range strings.Split(self.String(), LineBreak) {
if index == (lineCount - 1) {
break
}
lines = append(lines, Line(line))
}
return lines
}
func (self File) TailLines(lineCount int) []Line {
var lines []Line
fileLines := strings.Split(self.String(), LineBreak)
for index := len(fileLines) - 1; index >= 0; index-- {
if index == (lineCount - 1) {
break
}
lines = append(lines, Line(fileLines[index]))
}
return lines
}
func (self File) Head() []Line { return self.HeadLines(20) }
func (self File) Tail() []Line { return self.TailLines(20) }
///////////////////////////////////////////////////////////////////////////////
type OpenType int
const (
Append OpenType = iota
Overwrite
)
// TODO: This is actually quite important, we need to build this so that later
// we will be able to support variable chunk sizes. This is important for later
// streaming protocol plans.
// NOTE: This is a really good concept actually, and building abstraction around
// this like locking subtrees, writing subtrees, etc, would make a fantastic
// foundation for a RESP3 implementation for example; however we need to move
// forward. So we will just make a note about this structure and come back to it
// later
// TODO: My ideal chunking model would actually be a tree.
// It would reflect a merkle tree, and any subtree could be grabbed using
// the hash for that subtree. And it would return the offset and length,
// and ability to read those bytes.
type Chunk struct {
//Parent *Chunk
//ChildChunks []*Chunk
//NeighborChunk *Chunk
Lock *Lock
Index int
Offset int64
Length int64
//Checksum Hash
}
// NOTE After a lot of experimentation it just made sense to by default work
// within the chunking design pattern. Even when its not being used, just have
// that condition be achunk that is the offset 0, length -1, and a single
// chunk.
type Read struct {
File *File
Atomic bool // Use Locks
ReadAt time.Time
Chunks []Chunk
//Checksum Hash // Root of Merkle
}
func (self *File) Read() *Read {
return &Read{
File: self,
Atomic: true,
ReadAt: time.Now(),
Chunks: []Chunk{
Chunk{
Offset: 0,
Length: self.Size(),
},
},
}
}
// TODO: Not a fan of this, should be using uint's to save memory
func (self *File) ChunkedRead(chunkSize int64) *Read {
chunkCount := self.Size() / chunkSize
if (self.Size() % chunkSize) != 0 {
chunkCount += 1
}
var chunks = []Chunk{}
for index := int64(0); index < chunkCount; index++ {
chunks = append(chunks, Chunk{
Offset: (index * chunkSize),
Length: chunkSize,
})
}
return &Read{
File: self,
Atomic: true,
ReadAt: time.Now(),
Chunks: chunks,
}
}
func (self *File) ParallelRead(chunkCount int64) *Read {
chunkSize := self.Size() / chunkCount
if (self.Size() % chunkCount) != 0 {
chunkSize += 1
}
var chunks = []Chunk{}
for index := int64(0); index < chunkCount; index++ {
chunks = append(chunks, Chunk{
Offset: (index * chunkSize),
Length: chunkSize,
})
}
return &Read{
File: self,
Atomic: true,
Chunks: chunks,
}
}
func (self *Read) Path() Path { return self.File.Path() }
// TODO: In the future we use these to read each chunk in parallel using Go
// function and piece them back together using io.Reader's multireader
//func (self *Read) ReadSection() io.Reader {
// return io.NewSectionReader(self.File.ReadOnly(), self.Offset, self.Length)
//}
//func (self *Read) ReadChunk(chunkIndex int) io.Reader {
// return io.NewSectionReader(self.File.ReadOnly(), self.ChunkOffset(chunkIndex), self.Chunk)
//}
func (self *Read) Bytes() []byte {
var chunks []io.Reader
for _, chunk := range self.Chunks {
chunks = append(chunks, io.NewSectionReader(self.File.ReadOnly(), chunk.Offset, chunk.Length))
}
data := io.MultiReader(chunks...)
bytes, err := ioutil.ReadAll(data)
if err != nil {
panic(err)
}
return bytes
//buffer := bytes.NewBuffer([]byte{})
//size, err := io.Copy(buffer, data)
//if err != nil {
// panic(err)
//}
//return buffer.Bytes()
}
func (self *Read) String() string { return string(self.Bytes()) }
// WRITE //////////////////////////////////////////////////////////////////////
type Write struct {
File *File
Atomic bool // Use Locks
WriteAt time.Time
Chunks []Chunk
}
func (self *File) Write() *Write {
return &Write{
File: self,
Chunks: []Chunk{
Chunk{
Offset: 0,
Length: self.Size(),
},
},
}
}
func (self *Write) Bytes(data []byte) error {
file := self.File.WriteOnly()
_ = len(data)
err := ioutil.WriteFile(self.File.Path().String(), data, 0644)
file.Close()
return err
}
func (self *Write) String(s string) error {
file := self.File.WriteOnly()
bytesToWrite := len([]byte(s))
bytesWritten, err := file.WriteString(s)
if err != nil {
return err
}
if bytesWritten != bytesToWrite {
return fmt.Errorf("error: failed write all bytes: ", bytesWritten, " out of ", bytesToWrite)
}
return file.Close()
}
// FILE LOCKING ///////////////////////////////////////////////////////////////
// Trying to keep it as simple as possible, but supporting similar functionlaity
// and having similar API to Read/Write IO
type Lock struct {
Path Path
//File *File
Type int16
Timeout time.Duration
Atomic bool // Use Locks
LockedAt time.Time
Chunks []Chunk
}
//func (self *Lock) Chunks() int {
// chunks := (self.File.Size() / self.Chunk.Length)
// if (self.File.Size() % self.Chunk) != 0 {
// return (chunks + 1)
// } else {
// return chunks
// }
//}
func (self *Lock) WriteLock(chunk Chunk) *Lock {
file := File(self.Path).ReadOnly()
self.Type = syscall.F_WRLCK
self.Chunks = append(self.Chunks, chunk)
err := file.Close()
if err != nil {
panic(err)
}
return self
}
func (self *Lock) ReadLock(chunk Chunk) *Lock {
file := File(self.Path).ReadOnly()
self.Type = syscall.F_RDLCK
self.Chunks = append(self.Chunks, chunk)
err := file.Close()
if err != nil {
panic(err)
}
return self
}
func (self *Lock) Close(chunk Chunk) error {
file := File(self.Path).ReadOnly()
err := syscall.FcntlFlock(file.Fd(), syscall.F_SETLK, &syscall.Flock_t{
Type: self.Type,
Whence: int16(os.SEEK_SET),
Start: chunk.Offset,
Len: chunk.Length,
})
if err != nil {
panic(err)
}
return file.Close()
}
func (self *Lock) Open(chunk Chunk) error {
file := File(self.Path).ReadOnly()
err := syscall.FcntlFlock(file.Fd(), syscall.F_SETLK, &syscall.Flock_t{
Type: syscall.F_UNLCK,
Whence: int16(os.SEEK_SET),
Start: chunk.Offset,
Len: chunk.Length,
})
if err != nil {
panic(err)
}
return file.Close()
}
// Validation /////////////////////////////////////////////////////////////////
func (self Path) Clean() Path {
path := filepath.Clean(self.String())
if filepath.IsAbs(path) {
return Path(path)
} else {
path, _ = filepath.Abs(path)
return Path(path)
}
}
// TYPE CHECKS ////////////////////////////////////////////////////////////////
func (self Path) Exists() bool {
_, err := os.Stat(self.String())
return !os.IsNotExist(err)
}
func (self Path) IsDirectory() bool {
return self.Metadata().IsDir()
}
func (self Path) IsFile() bool {
return self.Metadata().Mode().IsRegular()
}