Compare commits
22 Commits
3a877c41a4
...
a9ef386883
Author | SHA1 | Date | |
---|---|---|---|
a9ef386883 | |||
24cfda82d7 | |||
655af5c446 | |||
b363a20a9d | |||
f5d326124d | |||
cb8fb2c0bc | |||
38119435f1 | |||
034a69c12f | |||
70006a3d7b | |||
8aada58d64 | |||
5d231207cd | |||
584d9426e6 | |||
7a772a5458 | |||
079c733b60 | |||
e24a8e9088 | |||
0b5d777077 | |||
75327286ef | |||
099b0cd849 | |||
c9c00e0072 | |||
b2ffb2062a | |||
2e8c825fff | |||
7b870950d1 |
@ -177,7 +177,7 @@ To cross compile, simply set the go environment variables. For example, for Pine
|
||||
make GOOS=linux GOARCH=arm64
|
||||
```
|
||||
|
||||
This will compile `itd` and `itctl` for Linux aarch64 which is what runs on the PinePhone. This daemon only runs on Linux due to the library's dependencies (`dbus`, `bluez`, and `playerctl` specifically).
|
||||
This will compile `itd` and `itctl` for Linux aarch64 which is what runs on the PinePhone. This daemon only runs on Linux due to the library's dependencies (`dbus`, and `bluez` specifically).
|
||||
|
||||
---
|
||||
|
||||
|
96
api/fs.go
Normal file
96
api/fs.go
Normal file
@ -0,0 +1,96 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
)
|
||||
|
||||
func (c *Client) Rename(old, new string) error {
|
||||
_, err := c.request(types.Request{
|
||||
Type: types.ReqTypeFS,
|
||||
Data: types.ReqDataFS{
|
||||
Type: types.FSTypeMove,
|
||||
Files: []string{old, new},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Remove(paths ...string) error {
|
||||
_, err := c.request(types.Request{
|
||||
Type: types.ReqTypeFS,
|
||||
Data: types.ReqDataFS{
|
||||
Type: types.FSTypeDelete,
|
||||
Files: paths,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Mkdir(paths ...string) error {
|
||||
_, err := c.request(types.Request{
|
||||
Type: types.ReqTypeFS,
|
||||
Data: types.ReqDataFS{
|
||||
Type: types.FSTypeMkdir,
|
||||
Files: paths,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ReadDir(path string) ([]types.FileInfo, error) {
|
||||
res, err := c.request(types.Request{
|
||||
Type: types.ReqTypeFS,
|
||||
Data: types.ReqDataFS{
|
||||
Type: types.FSTypeList,
|
||||
Files: []string{path},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []types.FileInfo
|
||||
err = mapstructure.Decode(res.Value, &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) ReadFile(path string) (string, error) {
|
||||
res, err := c.request(types.Request{
|
||||
Type: types.ReqTypeFS,
|
||||
Data: types.ReqDataFS{
|
||||
Type: types.FSTypeRead,
|
||||
Files: []string{path},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.Value.(string), nil
|
||||
}
|
||||
|
||||
func (c *Client) WriteFile(path, data string) error {
|
||||
_, err := c.request(types.Request{
|
||||
Type: types.ReqTypeFS,
|
||||
Data: types.ReqDataFS{
|
||||
Type: types.FSTypeWrite,
|
||||
Files: []string{path},
|
||||
Data: data,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
210
calls.go
210
calls.go
@ -1,85 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.arsenm.dev/infinitime"
|
||||
)
|
||||
|
||||
func initCallNotifs(dev *infinitime.Device) error {
|
||||
// Define rule to filter dbus messages
|
||||
rule := "type='signal',sender='org.freedesktop.ModemManager1',interface='org.freedesktop.ModemManager1.Modem.Voice',member='CallAdded'"
|
||||
|
||||
// Use dbus-monitor command with profiling output as a workaround
|
||||
// because go-bluetooth seems to monopolize the system bus connection
|
||||
// which makes monitoring show only bluez-related messages.
|
||||
cmd := exec.Command("dbus-monitor", "--system", "--profile", rule)
|
||||
// Get command output pipe
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Run command asynchronously
|
||||
err = cmd.Start()
|
||||
// Connect to system bus. This connection is for method calls.
|
||||
conn, err := newSystemBusConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create new scanner for command output
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
// Check if modem manager interface exists
|
||||
exists, err := modemManagerExists(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If it does not exist, stop function
|
||||
if !exists {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect to system bus. This connection is for monitoring.
|
||||
monitorConn, err := newSystemBusConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add match for new calls to monitor connection
|
||||
err = monitorConn.AddMatchSignal(
|
||||
dbus.WithMatchSender("org.freedesktop.ModemManager1"),
|
||||
dbus.WithMatchInterface("org.freedesktop.ModemManager1.Modem.Voice"),
|
||||
dbus.WithMatchMember("CallAdded"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create channel to receive calls
|
||||
callCh := make(chan *dbus.Message, 5)
|
||||
// Notify channel upon received message
|
||||
monitorConn.Eavesdrop(callCh)
|
||||
|
||||
var respHandlerOnce sync.Once
|
||||
var callObj dbus.BusObject
|
||||
|
||||
go func() {
|
||||
// For each line in output
|
||||
for scanner.Scan() {
|
||||
// Get line as string
|
||||
text := scanner.Text()
|
||||
// For every message received
|
||||
for event := range callCh {
|
||||
// Get path to call object
|
||||
callPath := event.Body[0].(dbus.ObjectPath)
|
||||
// Get call object
|
||||
callObj = conn.Object("org.freedesktop.ModemManager1", callPath)
|
||||
|
||||
// If line starts with "#", it is part of
|
||||
// the field format, skip it.
|
||||
if strings.HasPrefix(text, "#") {
|
||||
// Get phone number from call object using method call connection
|
||||
phoneNum, err := getPhoneNum(conn, callObj)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error getting phone number")
|
||||
continue
|
||||
}
|
||||
|
||||
// Split line into fields. The order is as follows:
|
||||
// type timestamp serial sender destination path interface member
|
||||
fields := strings.Fields(text)
|
||||
// Field 7 is Member. Make sure it is "CallAdded".
|
||||
if fields[7] == "CallAdded" {
|
||||
// Get Modem ID from modem path
|
||||
modemID := parseModemID(fields[5])
|
||||
// Get call ID of current call
|
||||
callID, err := getCurrentCallID(modemID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Get phone number of current call
|
||||
phoneNum, err := getPhoneNum(callID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Send call notification to PineTime
|
||||
// Send call notification to InfiniTime
|
||||
resCh, err := dev.NotifyCall(phoneNum)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
|
||||
go respHandlerOnce.Do(func() {
|
||||
// Wait for PineTime response
|
||||
res := <-resCh
|
||||
for res := range resCh {
|
||||
switch res {
|
||||
case infinitime.CallStatusAccepted:
|
||||
// Attempt to accept call
|
||||
err = acceptCall(callID)
|
||||
err = acceptCall(conn, callObj)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error accepting call")
|
||||
}
|
||||
case infinitime.CallStatusDeclined:
|
||||
// Attempt to decline call
|
||||
err = declineCall(callID)
|
||||
err = declineCall(conn, callObj)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error declining call")
|
||||
}
|
||||
@ -87,90 +92,51 @@ func initCallNotifs(dev *infinitime.Device) error {
|
||||
// Warn about unimplemented muting
|
||||
log.Warn().Msg("Muting calls is not implemented")
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info().Msg("Relaying calls to InfiniTime")
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseModemID(modemPath string) int {
|
||||
// Split path by "/"
|
||||
splitPath := strings.Split(modemPath, "/")
|
||||
// Get last element and convert to integer
|
||||
id, _ := strconv.Atoi(splitPath[len(splitPath)-1])
|
||||
return id
|
||||
func modemManagerExists(conn *dbus.Conn) (bool, error) {
|
||||
var names []string
|
||||
err := conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strSlcContains(names, "org.freedesktop.ModemManager1"), nil
|
||||
}
|
||||
|
||||
func getCurrentCallID(modemID int) (int, error) {
|
||||
// Create mmcli command
|
||||
cmd := exec.Command("mmcli", "--voice-list-calls", "-m", fmt.Sprint(modemID), "-J")
|
||||
// Run command and get output
|
||||
data, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var calls map[string][]string
|
||||
// Decode JSON from command output
|
||||
err = json.Unmarshal(data, &calls)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Get first call in output
|
||||
firstCall := calls["modem.voice.call"][0]
|
||||
// Split path by "/"
|
||||
splitCall := strings.Split(firstCall, "/")
|
||||
// Return last element converted to integer
|
||||
return strconv.Atoi(splitCall[len(splitCall)-1])
|
||||
}
|
||||
|
||||
func getPhoneNum(callID int) (string, error) {
|
||||
// Create dbus-send command
|
||||
cmd := exec.Command("dbus-send",
|
||||
"--dest=org.freedesktop.ModemManager1",
|
||||
"--system",
|
||||
"--print-reply=literal",
|
||||
"--type=method_call",
|
||||
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
|
||||
"org.freedesktop.DBus.Properties.Get",
|
||||
"string:org.freedesktop.ModemManager1.Call",
|
||||
"string:Number",
|
||||
)
|
||||
// Run command and get output
|
||||
numData, err := cmd.Output()
|
||||
// getPhoneNum gets a phone number from a call object using a DBus connection
|
||||
func getPhoneNum(conn *dbus.Conn, callObj dbus.BusObject) (string, error) {
|
||||
var out string
|
||||
// Get number property on DBus object and store return value in out
|
||||
err := callObj.StoreProperty("org.freedesktop.ModemManager1.Call.Number", &out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Split output into fields
|
||||
num := strings.Fields(string(numData))
|
||||
// Return last field
|
||||
return num[len(num)-1], nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func acceptCall(callID int) error {
|
||||
// Create dbus-send command
|
||||
cmd := exec.Command("dbus-send",
|
||||
"--dest=org.freedesktop.ModemManager1",
|
||||
"--print-reply",
|
||||
"--system",
|
||||
"--type=method_call",
|
||||
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
|
||||
"org.freedesktop.ModemManager1.Call.Accept",
|
||||
)
|
||||
// Run command and return errpr
|
||||
return cmd.Run()
|
||||
// getPhoneNum accepts a call using a DBus connection
|
||||
func acceptCall(conn *dbus.Conn, callObj dbus.BusObject) error {
|
||||
// Call Accept() method on DBus object
|
||||
call := callObj.Call("org.freedesktop.ModemManager1.Call.Accept", 0)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func declineCall(callID int) error {
|
||||
// Create dbus-send command
|
||||
cmd := exec.Command("dbus-send",
|
||||
"--dest=org.freedesktop.ModemManager1",
|
||||
"--print-reply",
|
||||
"--system",
|
||||
"--type=method_call",
|
||||
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
|
||||
"org.freedesktop.ModemManager1.Call.Hangup",
|
||||
)
|
||||
// Run command and return errpr
|
||||
return cmd.Run()
|
||||
// getPhoneNum declines a call using a DBus connection
|
||||
func declineCall(conn *dbus.Conn, callObj dbus.BusObject) error {
|
||||
// Call Hangup() method on DBus object
|
||||
call := callObj.Call("org.freedesktop.ModemManager1.Call.Hangup", 0)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
35
cmd/itctl/filesystem/filesystem.go
Normal file
35
cmd/itctl/filesystem/filesystem.go
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"go.arsenm.dev/itd/cmd/itctl/root"
|
||||
)
|
||||
|
||||
// filesystemCmd represents the filesystem command
|
||||
var filesystemCmd = &cobra.Command{
|
||||
Use: "filesystem",
|
||||
Aliases: []string{"fs"},
|
||||
Short: "Perform filesystem operations on the PineTime",
|
||||
}
|
||||
|
||||
func init() {
|
||||
root.RootCmd.AddCommand(filesystemCmd)
|
||||
}
|
56
cmd/itctl/filesystem/list.go
Normal file
56
cmd/itctl/filesystem/list.go
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// listCmd represents the list command
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list [path]",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List a directory",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
dirPath := "/"
|
||||
if len(args) > 0 {
|
||||
dirPath = args[0]
|
||||
}
|
||||
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
listing, err := client.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting directory listing")
|
||||
}
|
||||
|
||||
for _, entry := range listing {
|
||||
fmt.Println(entry)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
filesystemCmd.AddCommand(listCmd)
|
||||
}
|
49
cmd/itctl/filesystem/mkdir.go
Normal file
49
cmd/itctl/filesystem/mkdir.go
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// mkdirCmd represents the mkdir command
|
||||
var mkdirCmd = &cobra.Command{
|
||||
Use: "mkdir <path...>",
|
||||
Short: "Create a new directory",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
cmd.Usage()
|
||||
log.Fatal().Msg("Command mkdir requires one or more arguments")
|
||||
}
|
||||
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
err := client.Mkdir(args...)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error creating directory")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
filesystemCmd.AddCommand(mkdirCmd)
|
||||
}
|
50
cmd/itctl/filesystem/move.go
Normal file
50
cmd/itctl/filesystem/move.go
Normal file
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// moveCmd represents the move command
|
||||
var moveCmd = &cobra.Command{
|
||||
Use: "move <old> <new>",
|
||||
Aliases: []string{"mv"},
|
||||
Short: "Move a file or directory",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
cmd.Usage()
|
||||
log.Fatal().Msg("Command move requires two arguments")
|
||||
}
|
||||
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
err := client.Rename(args[0], args[1])
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error moving file or directory")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
filesystemCmd.AddCommand(moveCmd)
|
||||
}
|
73
cmd/itctl/filesystem/read.go
Normal file
73
cmd/itctl/filesystem/read.go
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// readCmd represents the read command
|
||||
var readCmd = &cobra.Command{
|
||||
Use: `read <remote path> <local path | "-">`,
|
||||
Short: "Read a file from InfiniTime",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
cmd.Usage()
|
||||
log.Fatal().Msg("Command read requires two arguments")
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
data, err := client.ReadFile(args[0])
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error moving file or directory")
|
||||
}
|
||||
|
||||
var suffix string
|
||||
var out *os.File
|
||||
if args[1] == "-" {
|
||||
out = os.Stdout
|
||||
suffix = "\n"
|
||||
} else {
|
||||
out, err = os.Create(args[1])
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error opening local file")
|
||||
}
|
||||
}
|
||||
|
||||
n, err := out.WriteString(data)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error writing to local file")
|
||||
}
|
||||
out.WriteString(suffix)
|
||||
|
||||
log.Info().Msgf("Read %d bytes in %s", n, time.Since(start))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
filesystemCmd.AddCommand(readCmd)
|
||||
}
|
50
cmd/itctl/filesystem/remove.go
Normal file
50
cmd/itctl/filesystem/remove.go
Normal file
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// removeCmd represents the remove command
|
||||
var removeCmd = &cobra.Command{
|
||||
Use: "remove <path...>",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Create a new directory",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
cmd.Usage()
|
||||
log.Fatal().Msg("Command mkdir requires one or more arguments")
|
||||
}
|
||||
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
err := client.Remove(args...)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error removing file or directory")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
filesystemCmd.AddCommand(removeCmd)
|
||||
}
|
72
cmd/itctl/filesystem/write.go
Normal file
72
cmd/itctl/filesystem/write.go
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// writeCmd represents the write command
|
||||
var writeCmd = &cobra.Command{
|
||||
Use: `write <local path | "-"> <remote path>`,
|
||||
Short: "Write a file to InfiniTime",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
cmd.Usage()
|
||||
log.Fatal().Msg("Command write requires two arguments")
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
var in *os.File
|
||||
if args[0] == "-" {
|
||||
in = os.Stdin
|
||||
} else {
|
||||
fl, err := os.Open(args[0])
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error opening local file")
|
||||
}
|
||||
in = fl
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(in)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error moving file or directory")
|
||||
}
|
||||
|
||||
err = client.WriteFile(args[1], string(data))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error writing to remote file")
|
||||
}
|
||||
|
||||
log.Info().Msgf("Wrote %d bytes in %s", len(data), time.Since(start))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
filesystemCmd.AddCommand(writeCmd)
|
||||
}
|
@ -25,6 +25,7 @@ import (
|
||||
"go.arsenm.dev/itd/cmd/itctl/root"
|
||||
_ "go.arsenm.dev/itd/cmd/itctl/set"
|
||||
_ "go.arsenm.dev/itd/cmd/itctl/watch"
|
||||
_ "go.arsenm.dev/itd/cmd/itctl/filesystem"
|
||||
|
||||
"os"
|
||||
|
||||
|
37
dbus.go
Normal file
37
dbus.go
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import "github.com/godbus/dbus/v5"
|
||||
|
||||
func newSystemBusConn() (*dbus.Conn, error) {
|
||||
// Connect to dbus session bus
|
||||
conn, err := dbus.SystemBusPrivate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Auth(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Hello()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func newSessionBusConn() (*dbus.Conn, error) {
|
||||
// Connect to dbus session bus
|
||||
conn, err := dbus.SessionBusPrivate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Auth(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Hello()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
2
go.mod
2
go.mod
@ -25,7 +25,7 @@ require (
|
||||
github.com/srwiley/oksvg v0.0.0-20210519022825-9fc0c575d5fe // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
|
||||
github.com/yuin/goldmark v1.4.1 // indirect
|
||||
go.arsenm.dev/infinitime v0.0.0-20211122091416-ec43bad46652
|
||||
go.arsenm.dev/infinitime v0.0.0-20211126043306-522c10a9c0a7
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||
golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0 // indirect
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
|
||||
|
4
go.sum
4
go.sum
@ -367,8 +367,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
|
||||
github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.arsenm.dev/infinitime v0.0.0-20211122091416-ec43bad46652 h1:2Z09crdXIs+aMy1xwuD6w2kml+EObfmzgCBaOxoZIv0=
|
||||
go.arsenm.dev/infinitime v0.0.0-20211122091416-ec43bad46652/go.mod h1:kNBKxQfqeLUfi13GM6tB1kSvLm8HlZ7PM47AYeJQIiw=
|
||||
go.arsenm.dev/infinitime v0.0.0-20211126043306-522c10a9c0a7 h1:m6BVtAiWMRbfUgBZVthXq2eZhSwDxdtOi8Dh/59hXik=
|
||||
go.arsenm.dev/infinitime v0.0.0-20211126043306-522c10a9c0a7/go.mod h1:TzAhsz7TAqEm/vWhgMvmIxHS5jt46hqKkPvr6cqvVyA=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||
|
@ -1,5 +1,10 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
ReqTypeHeartRate = iota
|
||||
ReqTypeBattLevel
|
||||
@ -15,6 +20,7 @@ const (
|
||||
ReqTypeStepCount
|
||||
ReqTypeWatchStepCount
|
||||
ReqTypeCancel
|
||||
ReqTypeFS
|
||||
)
|
||||
|
||||
const (
|
||||
@ -22,6 +28,21 @@ const (
|
||||
UpgradeTypeFiles
|
||||
)
|
||||
|
||||
const (
|
||||
FSTypeWrite = iota
|
||||
FSTypeRead
|
||||
FSTypeMove
|
||||
FSTypeDelete
|
||||
FSTypeList
|
||||
FSTypeMkdir
|
||||
)
|
||||
|
||||
type ReqDataFS struct {
|
||||
Type int `json:"type"`
|
||||
Files []string `json:"files"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type ReqDataFwUpgrade struct {
|
||||
Type int
|
||||
Files []string
|
||||
@ -56,3 +77,66 @@ type MotionValues struct {
|
||||
Y int16
|
||||
Z int16
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
IsDir bool `json:"isDir"`
|
||||
}
|
||||
|
||||
func (fi FileInfo) String() string {
|
||||
var isDirChar rune
|
||||
if fi.IsDir {
|
||||
isDirChar = 'd'
|
||||
} else {
|
||||
isDirChar = '-'
|
||||
}
|
||||
|
||||
// Get human-readable value for file size
|
||||
val, unit := bytesHuman(fi.Size)
|
||||
prec := 0
|
||||
// If value is less than 10, set precision to 1
|
||||
if val < 10 {
|
||||
prec = 1
|
||||
}
|
||||
// Convert float to string
|
||||
valStr := strconv.FormatFloat(val, 'f', prec, 64)
|
||||
|
||||
// Return string formatted like so:
|
||||
// - 10 kB file
|
||||
// or:
|
||||
// d 0 B .
|
||||
return fmt.Sprintf(
|
||||
"%c %3s %-2s %s",
|
||||
isDirChar,
|
||||
valStr,
|
||||
unit,
|
||||
fi.Name,
|
||||
)
|
||||
}
|
||||
|
||||
// bytesHuman returns a human-readable string for
|
||||
// the amount of bytes inputted.
|
||||
func bytesHuman(b int64) (float64, string) {
|
||||
const unit = 1000
|
||||
// Set possible units prefixes (PineTime flash is 4MB)
|
||||
units := [2]rune{'k', 'M'}
|
||||
// If amount of bytes is less than smallest unit
|
||||
if b < unit {
|
||||
// Return unchanged with unit "B"
|
||||
return float64(b), "B"
|
||||
}
|
||||
|
||||
div, exp := int64(unit), 0
|
||||
// Get decimal values and unit prefix index
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
|
||||
// Create string for full unit
|
||||
unitStr := string([]rune{units[exp], 'B'})
|
||||
|
||||
// Return decimal with unit string
|
||||
return float64(b) / float64(div), unitStr
|
||||
}
|
||||
|
8
main.go
8
main.go
@ -26,7 +26,11 @@ import (
|
||||
"go.arsenm.dev/infinitime"
|
||||
)
|
||||
|
||||
var firmwareUpdating = false
|
||||
var (
|
||||
firmwareUpdating = false
|
||||
// The FS must be updated when the watch is reconnected
|
||||
updateFS = false
|
||||
)
|
||||
|
||||
func main() {
|
||||
infinitime.Init()
|
||||
@ -61,6 +65,8 @@ func main() {
|
||||
log.Error().Err(err).Msg("Error sending notification to InfiniTime")
|
||||
}
|
||||
}
|
||||
|
||||
updateFS = true
|
||||
})
|
||||
|
||||
// Get firmware version
|
||||
|
47
music.go
47
music.go
@ -26,45 +26,22 @@ import (
|
||||
)
|
||||
|
||||
func initMusicCtrl(dev *infinitime.Device) error {
|
||||
// On player status change, set status
|
||||
err := player.Status(func(newStatus bool) {
|
||||
if !firmwareUpdating {
|
||||
dev.Music.SetStatus(newStatus)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
player.Init()
|
||||
|
||||
// On player title change, set track
|
||||
err = player.Metadata("title", func(newTitle string) {
|
||||
player.OnChange(func(ct player.ChangeType, val string) {
|
||||
if !firmwareUpdating {
|
||||
dev.Music.SetTrack(newTitle)
|
||||
switch ct {
|
||||
case player.ChangeTypeStatus:
|
||||
dev.Music.SetStatus(val == "Playing")
|
||||
case player.ChangeTypeTitle:
|
||||
dev.Music.SetTrack(val)
|
||||
case player.ChangeTypeAlbum:
|
||||
dev.Music.SetAlbum(val)
|
||||
case player.ChangeTypeArtist:
|
||||
dev.Music.SetArtist(val)
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// On player album change, set album
|
||||
err = player.Metadata("album", func(newAlbum string) {
|
||||
if !firmwareUpdating {
|
||||
dev.Music.SetAlbum(newAlbum)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// On player artist change, set artist
|
||||
err = player.Metadata("artist", func(newArtist string) {
|
||||
if !firmwareUpdating {
|
||||
dev.Music.SetArtist(newArtist)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for music events
|
||||
musicEvtCh, err := dev.Music.WatchEvents()
|
||||
|
@ -30,7 +30,7 @@ import (
|
||||
|
||||
func initNotifRelay(dev *infinitime.Device) error {
|
||||
// Connect to dbus session bus
|
||||
bus, err := dbus.SessionBus()
|
||||
bus, err := newSessionBusConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
143
socket.go
143
socket.go
@ -22,6 +22,7 @@ import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -32,6 +33,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/infinitime"
|
||||
"go.arsenm.dev/infinitime/blefs"
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
"go.arsenm.dev/itd/translit"
|
||||
)
|
||||
@ -78,6 +80,11 @@ func startSocket(dev *infinitime.Device) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fs, err := dev.FS()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error getting BLE filesystem")
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// Accept socket connection
|
||||
@ -87,7 +94,7 @@ func startSocket(dev *infinitime.Device) error {
|
||||
}
|
||||
|
||||
// Concurrently handle connection
|
||||
go handleConnection(conn, dev)
|
||||
go handleConnection(conn, dev, fs)
|
||||
}
|
||||
}()
|
||||
|
||||
@ -97,9 +104,23 @@ func startSocket(dev *infinitime.Device) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn, dev *infinitime.Device) {
|
||||
func handleConnection(conn net.Conn, dev *infinitime.Device, fs *blefs.FS) {
|
||||
defer conn.Close()
|
||||
|
||||
// If an FS update is required (reconnect ocurred)
|
||||
if updateFS {
|
||||
// Get new FS
|
||||
newFS, err := dev.FS()
|
||||
if err != nil {
|
||||
fs = nil
|
||||
log.Warn().Err(err).Msg("Error updating BLE filesystem")
|
||||
}
|
||||
// Set FS pointer to new FS
|
||||
*fs = *newFS
|
||||
// Reset updateFS
|
||||
updateFS = false
|
||||
}
|
||||
|
||||
// Create new scanner on connection
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
@ -431,6 +452,124 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
|
||||
break
|
||||
}
|
||||
firmwareUpdating = false
|
||||
case types.ReqTypeFS:
|
||||
if fs == nil {
|
||||
connErr(conn, req.Type, nil, "BLE filesystem is not available")
|
||||
break
|
||||
}
|
||||
// If no data, return error
|
||||
if req.Data == nil {
|
||||
connErr(conn, req.Type, nil, "Data required for filesystem operations")
|
||||
break
|
||||
}
|
||||
var reqData types.ReqDataFS
|
||||
// Decode data map to firmware upgrade request data
|
||||
err = mapstructure.Decode(req.Data, &reqData)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error decoding request data")
|
||||
break
|
||||
}
|
||||
switch reqData.Type {
|
||||
case types.FSTypeDelete:
|
||||
if len(reqData.Files) == 0 {
|
||||
connErr(conn, req.Type, nil, "Remove FS command requires at least one file")
|
||||
break
|
||||
}
|
||||
for _, file := range reqData.Files {
|
||||
err := fs.Remove(file)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error removing file")
|
||||
break
|
||||
}
|
||||
}
|
||||
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
|
||||
case types.FSTypeMove:
|
||||
if len(reqData.Files) != 2 {
|
||||
connErr(conn, req.Type, nil, "Move FS command requires an old path and new path in the files list")
|
||||
break
|
||||
}
|
||||
err := fs.Rename(reqData.Files[0], reqData.Files[1])
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error moving file")
|
||||
break
|
||||
}
|
||||
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
|
||||
case types.FSTypeMkdir:
|
||||
if len(reqData.Files) == 0 {
|
||||
connErr(conn, req.Type, nil, "Mkdir FS command requires at least one file")
|
||||
break
|
||||
}
|
||||
for _, file := range reqData.Files {
|
||||
err := fs.Mkdir(file)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error creating directory")
|
||||
break
|
||||
}
|
||||
}
|
||||
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
|
||||
case types.FSTypeList:
|
||||
if len(reqData.Files) != 1 {
|
||||
connErr(conn, req.Type, nil, "List FS command requires a path to list in the files list")
|
||||
break
|
||||
}
|
||||
entries, err := fs.ReadDir(reqData.Files[0])
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error reading directory")
|
||||
break
|
||||
}
|
||||
var out []types.FileInfo
|
||||
for _, entry := range entries {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting file info")
|
||||
break
|
||||
}
|
||||
out = append(out, types.FileInfo{
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
IsDir: info.IsDir(),
|
||||
})
|
||||
}
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
Value: out,
|
||||
})
|
||||
case types.FSTypeWrite:
|
||||
if len(reqData.Files) != 1 {
|
||||
connErr(conn, req.Type, nil, "Write FS command requires a path to the file to write")
|
||||
break
|
||||
}
|
||||
file, err := fs.Create(reqData.Files[0], uint32(len(reqData.Data)))
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error creating file")
|
||||
break
|
||||
}
|
||||
_, err = file.WriteString(reqData.Data)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error writing to file")
|
||||
break
|
||||
}
|
||||
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
|
||||
case types.FSTypeRead:
|
||||
if len(reqData.Files) != 1 {
|
||||
connErr(conn, req.Type, nil, "Read FS command requires a path to the file to read")
|
||||
break
|
||||
}
|
||||
file, err := fs.Open(reqData.Files[0])
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error opening file")
|
||||
break
|
||||
}
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error reading from file")
|
||||
break
|
||||
}
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
Value: string(data),
|
||||
})
|
||||
}
|
||||
case types.ReqTypeCancel:
|
||||
if req.Data == nil {
|
||||
connErr(conn, req.Type, nil, "No data provided. Cancel request requires request ID string as data.")
|
||||
|
Loading…
Reference in New Issue
Block a user