infinitime/blefs/file.go

370 lines
8.2 KiB
Go

package blefs
import (
"io"
"io/fs"
"path/filepath"
"time"
)
// File represents a file on the BLE filesystem
type File struct {
fs *FS
path string
offset uint32
offsetCh chan uint32
length uint32
amtLeft uint32
amtTferd uint32
isReadOnly bool
isWriteOnly bool
offsetChanged bool
}
// Open opens a file and returns it as an fs.File to
// satisfy the fs.FS interface
func (blefs *FS) Open(path string) (*File, error) {
// Make a read file request. This opens the file for reading.
err := blefs.request(
FSCmdReadFile,
true,
uint16(len(path)),
uint32(0),
uint32(blefs.maxData()),
path,
)
if err != nil {
return nil, err
}
return &File{
fs: blefs,
path: path,
length: 0,
offset: 0,
offsetCh: make(chan uint32, 5),
isReadOnly: true,
isWriteOnly: false,
}, nil
}
// Create makes a new file on the BLE file system and returns it.
func (blefs *FS) Create(path string, size uint32) (*File, error) {
// Make a write file request. This will create and open a file for writing.
err := blefs.request(
FSCmdWriteFile,
true,
uint16(len(path)),
uint32(0),
uint64(time.Now().UnixNano()),
size,
path,
)
if err != nil {
return nil, err
}
return &File{
fs: blefs,
path: path,
length: size,
amtLeft: size,
offset: 0,
offsetCh: make(chan uint32, 5),
isReadOnly: false,
isWriteOnly: true,
}, nil
}
// Size returns the total size of the opened file
func (file *File) Size() uint32 {
return file.length
}
// Progress returns a channel that receives the amount
// of bytes sent as they are sent
func (file *File) Progress() <-chan uint32 {
return file.offsetCh
}
// Read reads data from a file into b
func (fl *File) Read(b []byte) (n int, err error) {
// If file is write only (opened by FS.Create())
if fl.isWriteOnly {
return 0, ErrFileWriteOnly
}
// If offset has been changed (Seek() called)
if fl.offsetChanged {
// Create new read file request with the specified offset to restart reading
err := fl.fs.request(
FSCmdReadFile,
true,
uint16(len(fl.path)),
fl.offset,
uint32(fl.fs.maxData()),
fl.path,
)
if err != nil {
return 0, err
}
// Reset offsetChanged
fl.offsetChanged = false
}
// Get length of b. This will be the maximum amount that can be read.
maxLen := uint32(len(b))
if maxLen == 0 {
return 0, nil
}
var buf []byte
for {
// If amount transfered equals max length
if fl.amtTferd == maxLen {
// Reset amount transfered
fl.amtTferd = 0
// Copy buffer contents to b
copy(b, buf)
// Return max length with no error
return int(maxLen), nil
}
// Create new empty fileReadResponse
resp := fileReadResponse{}
// Upon receiving 0x11 (FSResponseReadFile)
err := fl.fs.on(FSResponseReadFile, func(data []byte) error {
// Read binary data into struct
err := decode(
data,
&resp.status,
&resp.padding,
&resp.offset,
&resp.length,
&resp.chunkLen,
&resp.data,
)
if err != nil {
return err
}
// If status is not ok
if resp.status != FSStatusOk {
return FSError{resp.status}
}
return nil
})
if err != nil {
return 0, err
}
// If entire file transferred, break
if fl.offset == resp.length {
break
}
// Append data returned in response to buffer
buf = append(buf, resp.data...)
// Set file length
fl.length = resp.length
// Add returned chunk length to offset and amount transferred
fl.offset += resp.chunkLen
fl.offsetCh <- fl.offset
fl.amtTferd += resp.chunkLen
// Calculate amount of bytes to be sent in next request
chunkLen := min(fl.length-fl.offset, uint32(fl.fs.maxData()))
// If after transferring, there will be more data than max length
if fl.amtTferd+chunkLen > maxLen {
// Set chunk length to amount left to fill max length
chunkLen = maxLen - fl.amtTferd
}
// Make data request. This will return more data from the file.
fl.fs.request(
FSCmdDataReq,
false,
byte(FSStatusOk),
padding(2),
fl.offset,
chunkLen,
)
}
close(fl.offsetCh)
// Copy buffer contents to b
copied := copy(b, buf)
// Return amount of bytes copied with EOF error
return copied, io.EOF
}
// Write writes data from b into a file on the BLE filesysyem
func (fl *File) Write(b []byte) (n int, err error) {
maxLen := uint32(cap(b))
// If file is read only (opened by FS.Open())
if fl.isReadOnly {
return 0, ErrFileReadOnly
}
// If offset has been changed (Seek() called)
if fl.offsetChanged {
// Create new write file request with the specified offset to restart writing
err := fl.fs.request(
FSCmdWriteFile,
true,
uint16(len(fl.path)),
fl.offset,
uint64(time.Now().UnixNano()),
fl.length,
fl.path,
)
if err != nil {
return 0, err
}
// Reset offsetChanged
fl.offsetChanged = false
}
for {
// If amount transfered equals max length
if fl.amtTferd == maxLen {
// Reset amount transfered
fl.amtTferd = 0
// Return max length with no error
return int(maxLen), nil
}
// Create new empty fileWriteResponse
resp := fileWriteResponse{}
// Upon receiving 0x21 (FSResponseWriteFile)
err := fl.fs.on(FSResponseWriteFile, func(data []byte) error {
// Read binary data into struct
err := decode(
data,
&resp.status,
&resp.padding,
&resp.offset,
&resp.modtime,
&resp.free,
)
if err != nil {
return err
}
// If status is not ok
if resp.status != FSStatusOk {
return FSError{resp.status}
}
return nil
})
if err != nil {
return 0, err
}
// If no free space left in current file, break
if resp.free == 0 {
break
}
// Calculate amount of bytes to be transferred in next request
chunkLen := min(fl.length-fl.offset, uint32(fl.fs.maxData()))
// If after transferring, there will be more data than max length
if fl.amtTferd+chunkLen > maxLen {
// Set chunk length to amount left to fill max length
chunkLen = maxLen - fl.amtTferd
}
// Get data from b
chunk := b[fl.amtTferd : fl.amtTferd+chunkLen]
// Create transfer request. This will transfer the chunk to the file.
fl.fs.request(
FSCmdTransfer,
false,
byte(FSStatusOk),
padding(2),
fl.offset,
chunkLen,
chunk,
)
// Add chunk length to offset and amount transferred
fl.offset += chunkLen
fl.offsetCh <- fl.offset
fl.amtTferd += chunkLen
}
close(fl.offsetCh)
return int(fl.offset), nil
}
// WriteString converts the string to []byte and calls Write()
func (fl *File) WriteString(s string) (n int, err error) {
return fl.Write([]byte(s))
}
// Seek changes the offset of the file being read/written.
// This can only be done once in between reads/writes.
func (fl *File) Seek(offset int64, whence int) (int64, error) {
if fl.offsetChanged {
return int64(fl.offset), ErrOffsetChanged
}
var newOffset int64
switch whence {
case io.SeekCurrent:
newOffset = int64(fl.offset) + offset
case io.SeekStart:
newOffset = offset
case io.SeekEnd:
newOffset = int64(fl.length) + offset
default:
newOffset = int64(fl.offset)
}
if newOffset < 0 || uint32(newOffset) > fl.length {
return int64(fl.offset), ErrInvalidOffset
}
fl.offset = uint32(newOffset)
fl.offsetChanged = true
return int64(newOffset), nil
}
// Close implements the fs.File interface.
// It just returns nil.
func (fl *File) Close() error {
return nil
}
// Stat does a RedDir() and finds the current file in the output
func (fl *File) Stat() (fs.FileInfo, error) {
// Get directory in filepath
dir := filepath.Dir(fl.path)
// Read directory
dirEntries, err := fl.fs.ReadDir(dir)
if err != nil {
return nil, err
}
for _, entry := range dirEntries {
// If file name is base name of path
if entry.Name() == filepath.Base(fl.path) {
// Return file info
return entry.Info()
}
}
return nil, ErrFileNotExists
}
// fileReadResponse represents a response for a read request
type fileReadResponse struct {
status int8
padding [2]byte
offset uint32
length uint32
chunkLen uint32
data []byte
}
// fileWriteResponse represents a response for a write request
type fileWriteResponse struct {
status int8
padding [2]byte
offset uint32
modtime uint64
free uint32
}
// min returns the smaller uint32 out of two given
func min(o, t uint32) uint32 {
if t < o {
return t
}
return o
}