mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
Cleanup the web ui/flow
This commit is contained in:
@@ -69,6 +69,7 @@ The `soundtouch-service` is a local server that emulates Bose's cloud services.
|
||||
|
||||
#### Key Features:
|
||||
- **🏠 Local Emulation**: BMX and Marge service implementation
|
||||
- **🔌 Easy Setup**: Activate SSH via USB stick (`remote_services` file)
|
||||
- **🔧 Device Migration**: Seamlessly transition devices to local control
|
||||
- **🌐 Web Management UI**: Easy browser-based setup and management
|
||||
- **💾 Persistent Data**: Store presets, recents, and sources locally
|
||||
@@ -79,12 +80,14 @@ The `soundtouch-service` is a local server that emulates Bose's cloud services.
|
||||
# Start the service
|
||||
soundtouch-service
|
||||
```
|
||||
Open `http://localhost:8000` in your browser to manage your devices.
|
||||
Open `http://localhost:8000` in your browser to manage your devices. Documentation is also available directly through the web interface.
|
||||
|
||||
For a comprehensive guide on transitioning your system, see the [Bose Cloud Shutdown: Survival Guide](docs/CLOUD-SHUTDOWN-GUIDE.md).
|
||||
|
||||
Detailed service configuration and Docker instructions can be found in [docs/SOUNDTOUCH-SERVICE.md](docs/SOUNDTOUCH-SERVICE.md).
|
||||
|
||||
For professional migration tips and safety measures, see the [Migration & Safety Guide](docs/MIGRATION-SAFETY-GUIDE.md).
|
||||
|
||||
### Library Usage
|
||||
|
||||
#### Basic Control
|
||||
|
||||
@@ -388,6 +388,7 @@ func setupRouter(server *handlers.Server, pyProxy *httputil.ReverseProxy) *chi.M
|
||||
|
||||
r.Get("/media/*", server.HandleMedia())
|
||||
r.Get("/web/*", server.HandleWeb())
|
||||
r.Get("/docs/*", server.HandleDocs)
|
||||
|
||||
r.Route("/bmx", func(r chi.Router) {
|
||||
r.Get("/registry/v1/services", server.HandleBMXRegistry)
|
||||
|
||||
@@ -43,13 +43,13 @@ To migrate your speakers, the service needs SSH access. You can enable it by:
|
||||
3. Rebooting the speaker.
|
||||
Once enabled, you can log in as `root` (no password).
|
||||
|
||||
#### 4. Discover and Sync Device Data
|
||||
The web interface handles the entire process in a guided flow across four tabs:
|
||||
#### 4. Setup Through the Web UI
|
||||
The web interface handles the entire process in a guided flow. Before proceeding, we strongly recommend reviewing the [Migration & Safety Guide](MIGRATION-SAFETY-GUIDE.md).
|
||||
|
||||
* **Step 1: Devices**: The service automatically scans for SoundTouch devices on your network. If a device is not found, you can manually add its IP address.
|
||||
* **Step 2: Data Sync**: Select your device and click "Start Sync". This will automatically fetch your presets, recents, and configured sources from the speaker and store them in the local `data/` directory.
|
||||
* **Step 3: Migration**: Choose your redirection method (XML Recommended) and click "Confirm Migration & Reboot".
|
||||
* **Step 4: Settings**: Configure global server URLs and proxy behavior (logging, redaction).
|
||||
* **Step 1: Settings**: Configure your server's IP or domain. This ensures the speakers know where to find the local services.
|
||||
* **Step 2: Devices**: The service automatically scans for SoundTouch devices on your network. If a device is not found, you can manually add its IP address.
|
||||
* **Step 3: Data Sync**: Select your device and click "Start Sync". This will automatically fetch your presets, recents, and configured sources from the speaker and store them in the local `data/` directory.
|
||||
* **Step 4: Migration**: Choose your redirection method (XML Recommended) and click "Confirm Migration". After the migration, reboot your speaker to apply the changes.
|
||||
|
||||
#### 5. Verify Your Local Data
|
||||
Once migrated, your speaker will use the data captured during the Sync step.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
### Professional Migration & Safety Guide
|
||||
|
||||
Starting a migration on real hardware requires a "Safety First" approach. This guide outlines the safety features implemented in the `soundtouch-service` and provides a checklist for a successful migration.
|
||||
|
||||
#### 🛠 Technical Safety Enhancements
|
||||
|
||||
The following features are built into the `soundtouch-service` to ensure stability and easy rollbacks:
|
||||
|
||||
1. **Off-Device Backups**: Before any migration starts, the service automatically fetches the original `SoundTouchSdkPrivateCfg.xml` and `/etc/hosts` from your speaker and saves them locally in your `data/default/devices/<SERIAL>/` directory. This ensures you have a recovery path even if the speaker's filesystem becomes inaccessible.
|
||||
2. **Pre-flight Write Verification**: The migration process includes a mandatory check for SSH write access (`rw`) before attempting any modifications. This prevents "half-baked" migrations where a script might fail halfway through due to a read-only filesystem.
|
||||
3. **Automatic Safety on Sync**: Running a "Sync" in the Web UI or CLI automatically triggers an off-device backup, making it the perfect first step for any new device discovery.
|
||||
|
||||
#### 📋 Professional Migration Checklist
|
||||
|
||||
Before you proceed with the actual migration, follow these steps:
|
||||
|
||||
1. **Enable SSH Access (Prerequisite)**: This toolkit requires SSH access to your speakers, which is not enabled by default.
|
||||
- Create an empty file named `remote_services` on a USB stick.
|
||||
- Insert the USB stick into the SoundTouch speaker's **SERVICE** port.
|
||||
- Reboot the speaker (unplug and replug).
|
||||
- The speaker will now allow SSH connections as `root` with no password.
|
||||
2. **Network Isolation (Optional but Recommended)**: Ensure the device is on a stable wired connection if possible, or a dedicated 2.4GHz SSID to avoid drops during SSH operations.
|
||||
3. **Initial Discovery & Sync**:
|
||||
- Run `soundtouch-cli discover devices` to ensure connectivity.
|
||||
- Use the Web UI or CLI to "Sync" the device. This will automatically backup your presets and system configuration files to your local server.
|
||||
4. **Validate SSH Access**: Confirm the device responds to SSH without a password (the service handles this, but a manual check with `ssh root@<IP>` is a good sanity check).
|
||||
5. **Use XML Migration First**: The `XML` migration method is less invasive than the `Hosts` method. It only changes the application config and doesn't require modifying the system's DNS/CA trust store if you don't need full HTTPS interception initially.
|
||||
6. **Monitor Logs**: Run the `soundtouch-service` with `DEBUG` or `INFO` logging to see the step-by-step progress of the migration.
|
||||
|
||||
#### 🔄 Rollback Strategy
|
||||
|
||||
If something goes wrong or you want to return to the original Bose cloud services:
|
||||
|
||||
* **Standard Revert**: Use the "Revert Migration" button in the Web UI or the corresponding CLI command. This restores the `.original` files created on the device.
|
||||
* **Emergency Recovery**: If the device is unreachable via the UI but SSH still works, you can manually restore the files from your local `data/` directory using `scp` or the backups created on-device (`.original`).
|
||||
* **Factory Reset**: As a last resort, Bose SoundTouch devices can be factory reset (usually by holding '1' and 'Volume Down' while plugging in). This will wipe all settings and return the device to the stock firmware configuration (the firmware itself remains at the current version, but configurations are reset).
|
||||
|
||||
By using the built-in off-device backups and pre-flight checks, the risk of "bricking" or losing configuration during the transition is significantly reduced.
|
||||
@@ -0,0 +1,64 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
// HandleDocs returns a handler for serving documentation files as HTML.
|
||||
func (s *Server) HandleDocs(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/docs")
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
if path == "" {
|
||||
path = "CLOUD-SHUTDOWN-GUIDE.md"
|
||||
}
|
||||
|
||||
// Ensure we only serve files from the docs directory
|
||||
filePath := filepath.Join("docs", path)
|
||||
if !strings.HasPrefix(filepath.Clean(filePath), "docs") {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Render markdown to HTML
|
||||
output := blackfriday.Run(content)
|
||||
|
||||
// Wrap in a simple HTML template
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>%s - Bose SoundTouch Toolkit Docs</title>
|
||||
<link rel="icon" href="/media/favicon-braille.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/web/css/style.css">
|
||||
<style>
|
||||
body { max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.6; color: #333; }
|
||||
h1, h2, h3 { color: #2196F3; }
|
||||
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||
code { font-family: monospace; background: #eee; padding: 2px 4px; border-radius: 3px; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #2196F3; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.back-link { margin-bottom: 20px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/" class="back-link">← Back to Toolkit</a>
|
||||
<div class="markdown-body">
|
||||
%s
|
||||
</div>
|
||||
</body>
|
||||
</html>`, path, output)
|
||||
}
|
||||
@@ -34,3 +34,35 @@ pre { background-color: #eee; padding: 10px; overflow-x: auto; font-size: 12px;
|
||||
padding: 5px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
/* Guide & Overview styles */
|
||||
.guide-steps li {
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.guide-steps strong {
|
||||
color: #2196F3;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.info-box {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.prerequisite-box {
|
||||
background-color: #fffde7;
|
||||
border-left: 4px solid #fff176;
|
||||
}
|
||||
.safety-box {
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.info-box a {
|
||||
color: #0d47a1;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.info-box a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,89 @@
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-btn active" onclick="openTab(event, 'tab-devices')">1. Devices</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'tab-sync')">2. Data Sync</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'tab-migration')">3. Migration</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'tab-settings')">4. Settings</button>
|
||||
<button class="tab-btn active" onclick="openTab(event, 'tab-overview')">Overview</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'tab-settings')">1. Settings</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'tab-devices')">2. Devices</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'tab-sync')">3. Data Sync</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'tab-migration')">4. Migration</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: Devices -->
|
||||
<div id="tab-devices" class="tab-content active">
|
||||
<!-- Tab 0: Overview -->
|
||||
<div id="tab-overview" class="tab-content active">
|
||||
<h2>Welcome to Bose SoundTouch Toolkit</h2>
|
||||
<p>This toolkit helps you keep your Bose SoundTouch speakers functional even after the Bose Cloud shutdown in May 2026. It emulates the necessary cloud services locally on your network.</p>
|
||||
|
||||
<h3>Migration Process at a Glance</h3>
|
||||
<div class="info-box prerequisite-box">
|
||||
<strong>🔌 Prerequisite: Enable SSH</strong><br>
|
||||
Migration requires SSH access. To enable it:
|
||||
<ol style="margin-top: 5px; margin-bottom: 0;">
|
||||
<li>Create an empty file named <code>remote_services</code> on a USB stick.</li>
|
||||
<li>Insert it into the speaker's <strong>SERVICE</strong> port and reboot the speaker.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<ol class="guide-steps">
|
||||
<li>
|
||||
<strong>Settings:</strong> Review the <strong>Settings</strong> tab. Ensure the "Target Domain" and "Proxy Domain" use an IP address or domain name that is <strong>accessible from your speakers</strong> (usually the IP of this server on your local network).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Discovery:</strong> Go to the <strong>Devices</strong> tab to find your speakers on the network.
|
||||
Ensure your speakers are powered on and connected to the same network.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Data Sync:</strong> In the <strong>Data Sync</strong> tab, fetch your current presets, recents, and sources.
|
||||
This step is critical to ensure your local service has all your personalized data before you disconnect from the Bose cloud.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Migration:</strong> In the <strong>Migration</strong> tab, redirect your speaker to this local service.
|
||||
We recommend the <strong>XML Configuration</strong> method as it is surgical and easily reversible.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Verification:</strong> After migration and reboot, your speaker will communicate with this toolkit instead of Bose servers.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="info-box safety-box">
|
||||
<strong>⚠️ Safety First:</strong> Before starting any migration, please read our
|
||||
<a href="/docs/MIGRATION-SAFETY-GUIDE.md" target="_blank">Professional Migration & Safety Guide</a>.
|
||||
The toolkit automatically creates backups, but understanding the process is key to a smooth transition.
|
||||
</div>
|
||||
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="/docs/CLOUD-SHUTDOWN-GUIDE.md" target="_blank">Cloud Shutdown Survival Guide</a></li>
|
||||
<li><a href="/docs/CLI-REFERENCE.md" target="_blank">CLI Reference</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: Settings -->
|
||||
<div id="tab-settings" class="tab-content">
|
||||
<h2>System Settings</h2>
|
||||
<p style="font-size: 0.9em; color: #555; margin-bottom: 20px;">
|
||||
<strong>Note:</strong> These URLs must be <strong>accessible from your SoundTouch devices</strong>.
|
||||
Use the IP address of this server on your local network (e.g., <code>http://192.168.1.100:8000</code>)
|
||||
rather than <code>localhost</code>.
|
||||
</p>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="target-domain">Target Domain:</label>
|
||||
<input type="text" id="target-domain" placeholder="http://192.168.x.x:8000" style="width: 300px;">
|
||||
<span style="font-size: 0.8em; color: #666;">(Standard services URL)</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="proxy-domain">Proxy Domain:</label>
|
||||
<input type="text" id="proxy-domain" placeholder="http://192.168.x.x:8000" style="width: 300px;">
|
||||
<span style="font-size: 0.8em; color: #666;">(Upstream proxy URL)</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
Proxy Logging:
|
||||
<label><input type="checkbox" id="proxy-redact" onchange="updateProxySettings()"> Redact Sensitive Headers</label>
|
||||
<label style="margin-left: 15px;"><input type="checkbox" id="proxy-log-body" onchange="updateProxySettings()"> Log Bodies</label>
|
||||
<label style="margin-left: 15px;"><input type="checkbox" id="proxy-record" onchange="updateProxySettings()"> Record Interactions</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Devices -->
|
||||
<div id="tab-devices" class="tab-content">
|
||||
<h2>Known Devices <span id="discovery-indicator" style="font-size: 0.5em; vertical-align: middle; display: none;">🔍 Scanning...</span></h2>
|
||||
<div id="device-list">Loading devices...</div>
|
||||
<div style="margin-top: 20px;">
|
||||
@@ -28,7 +103,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Data Sync -->
|
||||
<!-- Tab 3: Data Sync -->
|
||||
<div id="tab-sync" class="tab-content">
|
||||
<h2>Initial Data Sync</h2>
|
||||
<p>Before migrating, fetch your presets, recents, and configured sources from the device to ensure they are available locally.</p>
|
||||
@@ -46,7 +121,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Migration -->
|
||||
<!-- Tab 4: Migration -->
|
||||
<div id="tab-migration" class="tab-content">
|
||||
<h2>Device Migration</h2>
|
||||
<div class="device-selection">
|
||||
@@ -183,27 +258,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 4: Settings -->
|
||||
<div id="tab-settings" class="tab-content">
|
||||
<h2>System Settings</h2>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="target-domain">Target Domain:</label>
|
||||
<input type="text" id="target-domain" placeholder="http://localhost:8000" style="width: 300px;">
|
||||
<span style="font-size: 0.8em; color: #666;">(This URL will be used for standard services)</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="proxy-domain">Proxy Domain:</label>
|
||||
<input type="text" id="proxy-domain" placeholder="http://localhost:8000" style="width: 300px;">
|
||||
<span style="font-size: 0.8em; color: #666;">(This URL will be used to proxy upstream Bose services)</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
Proxy Logging:
|
||||
<label><input type="checkbox" id="proxy-redact" onchange="updateProxySettings()"> Redact Sensitive Headers</label>
|
||||
<label style="margin-left: 15px;"><input type="checkbox" id="proxy-log-body" onchange="updateProxySettings()"> Log Bodies</label>
|
||||
<label style="margin-left: 15px;"><input type="checkbox" id="proxy-record" onchange="updateProxySettings()"> Record Interactions</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/web/js/script.js"></script>
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -433,11 +434,28 @@ func (m *Manager) MigrateSpeaker(deviceIP, targetURL, proxyURL string, options m
|
||||
method = MigrationMethodXML
|
||||
}
|
||||
|
||||
if method == MigrationMethodHosts {
|
||||
return m.migrateViaHosts(deviceIP, targetURL)
|
||||
var logs string
|
||||
|
||||
// 0. Off-device backup for safety
|
||||
if backupErr := m.BackupConfigOffDevice(deviceIP); backupErr != nil {
|
||||
logs += fmt.Sprintf("Warning: Failed to create off-device backup: %v\n", backupErr)
|
||||
// We continue, but this is a warning
|
||||
} else {
|
||||
logs += "Successfully created off-device backup of current configuration.\n"
|
||||
}
|
||||
|
||||
var logs string
|
||||
// 0b. Pre-flight check for SSH /rw permissions
|
||||
client := m.NewSSH(deviceIP)
|
||||
rwCmd := "(rw || mount -o remount,rw /)"
|
||||
if rwTest, rwErr := client.Run(rwCmd); rwErr != nil {
|
||||
return logs, fmt.Errorf("pre-flight check failed: cannot gain write access (cmd: %s, output: %s): %w", rwCmd, rwTest, rwErr)
|
||||
}
|
||||
logs += "Pre-flight: Write access verified.\n"
|
||||
|
||||
if method == MigrationMethodHosts {
|
||||
out, err := m.migrateViaHosts(deviceIP, targetURL)
|
||||
return logs + out, err
|
||||
}
|
||||
|
||||
out, err := m.EnsureRemoteServices(deviceIP)
|
||||
logs += "Ensuring remote services:\n" + out + "\n"
|
||||
@@ -459,7 +477,6 @@ func (m *Manager) MigrateSpeaker(deviceIP, targetURL, proxyURL string, options m
|
||||
}
|
||||
|
||||
// If we have a proxyURL and can read current config, use it
|
||||
client := m.NewSSH(deviceIP)
|
||||
if curCfg, curCfgErr := client.Run(fmt.Sprintf("cat %s", SoundTouchSdkPrivateCfgPath)); curCfgErr == nil && curCfg != "" {
|
||||
logs += "Read current configuration\n"
|
||||
|
||||
@@ -490,7 +507,6 @@ func (m *Manager) MigrateSpeaker(deviceIP, targetURL, proxyURL string, options m
|
||||
|
||||
// 0. Backup original config if it doesn't exist
|
||||
remotePath := SoundTouchSdkPrivateCfgPath
|
||||
rwCmd := "(rw || mount -o remount,rw /)"
|
||||
|
||||
if backupOut, err := client.Run(fmt.Sprintf("[ -f %s.original ]", remotePath)); err != nil {
|
||||
logs += fmt.Sprintf("Backing up original config to %s.original (check: %s)\n", remotePath, backupOut)
|
||||
@@ -531,6 +547,49 @@ func (m *Manager) MigrateSpeaker(deviceIP, targetURL, proxyURL string, options m
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// BackupConfigOffDevice creates a local backup of the speaker's configuration files in the DataStore.
|
||||
func (m *Manager) BackupConfigOffDevice(deviceIP string) error {
|
||||
if m.DataStore == nil {
|
||||
return fmt.Errorf("datastore not configured")
|
||||
}
|
||||
|
||||
client := m.NewSSH(deviceIP)
|
||||
|
||||
// We need the serial number to find the right directory in DataStore
|
||||
info, err := m.GetLiveDeviceInfo(deviceIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get device info: %w", err)
|
||||
}
|
||||
|
||||
serial := info.SerialNumber
|
||||
if serial == "" {
|
||||
return fmt.Errorf("could not determine device serial number")
|
||||
}
|
||||
|
||||
deviceDir := m.DataStore.AccountDeviceDir("default", serial)
|
||||
if err := os.MkdirAll(deviceDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create device directory: %w", err)
|
||||
}
|
||||
|
||||
// 1. Backup SoundTouchSdkPrivateCfg.xml
|
||||
if config, err := client.Run(fmt.Sprintf("cat %s", SoundTouchSdkPrivateCfgPath)); err == nil && config != "" {
|
||||
backupPath := filepath.Join(deviceDir, "SoundTouchSdkPrivateCfg.xml.bak")
|
||||
if err := os.WriteFile(backupPath, []byte(config), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config backup: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Backup /etc/hosts
|
||||
if hosts, err := client.Run("cat /etc/hosts"); err == nil && hosts != "" {
|
||||
backupPath := filepath.Join(deviceDir, "hosts.bak")
|
||||
if err := os.WriteFile(backupPath, []byte(hosts), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write hosts backup: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupConfig creates a backup of the current configuration on the speaker.
|
||||
func (m *Manager) BackupConfig(deviceIP string) (string, error) {
|
||||
client := m.NewSSH(deviceIP)
|
||||
@@ -1162,6 +1221,9 @@ func (m *Manager) SyncDeviceData(deviceIP string) error {
|
||||
// 4. Fetch Sources
|
||||
m.syncSources(deviceIP, accountID, deviceID)
|
||||
|
||||
// 5. Create off-device backup of system configuration
|
||||
_ = m.BackupConfigOffDevice(deviceIP)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user