351 lines
7.8 KiB
Go
351 lines
7.8 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
|
||
|
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) (fs.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,
|
||
|
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,
|
||
|
isReadOnly: false,
|
||
|
isWriteOnly: true,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// 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.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,
|
||
|
)
|
||
|
}
|
||
|
// 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.amtTferd += chunkLen
|
||
|
}
|
||
|
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
|
||
|
}
|