package blefs import ( "io" "io/fs" "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) { if blefs.readOpen { return nil, ErrReadOpen } blefs.readOpen = true // 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) { if blefs.writeOpen { return nil, ErrWriteOpen } blefs.writeOpen = true // 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.amtTferd), 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 must be called before opening another file func (fl *File) Close() error { if fl.isReadOnly { fl.fs.readOpen = false } else if fl.isWriteOnly { fl.fs.writeOpen = false } return nil } // Stat does a ReadDir() and finds the current file in the output func (fl *File) Stat() (fs.FileInfo, error) { return fl.fs.Stat(fl.path) } // 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 }