import/export interfaces

This commit is contained in:
Sudo-Ivan 2024-12-30 19:32:41 -06:00
commit 06067cc39f
2 changed files with 336 additions and 1 deletions

View file

@ -587,6 +587,173 @@ class ReticulumMeshChat:
return web.json_response({
"message": "Interface has been added",
})
# export interfaces
@routes.get("/api/v1/reticulum/interfaces/export")
async def export_interfaces(request):
try:
output = []
for name, interface in self.reticulum.config["interfaces"].items():
output.append(f"[[{name}]]")
for key, value in interface.items():
output.append(f" {key} = {value}")
output.append("")
return web.Response(
text="\n".join(output),
content_type="text/plain",
headers={
"Content-Disposition": "attachment; filename=reticulum_interfaces"
}
)
except Exception as e:
print(f"Export error: {str(e)}")
return web.json_response({
"message": f"Failed to export interfaces: {str(e)}"
}, status=500)
# preview importable interfaces
@routes.post("/api/v1/reticulum/interfaces/preview")
async def preview_interfaces(request):
try:
reader = await request.multipart()
field = await reader.next()
if field.name == 'config':
config_text = ''
while True:
chunk = await field.read_chunk()
if not chunk:
break
config_text += chunk.decode('utf-8')
interfaces = []
current_interface = None
for line in config_text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("[[") and line.endswith("]]"):
if current_interface:
interfaces.append(current_interface)
name = line[2:-2]
current_interface = {
"name": name,
"type": None
}
elif current_interface is not None and "=" in line:
key, value = [x.strip() for x in line.split("=", 1)]
if key == "type":
current_interface["type"] = value
if current_interface:
interfaces.append(current_interface)
return web.json_response({
"interfaces": interfaces
})
except Exception as e:
print(f"Preview error: {str(e)}")
return web.json_response({
"message": f"Failed to parse config file: {str(e)}"
}, status=500)
# import interfaces from config
@routes.post("/api/v1/reticulum/interfaces/import")
async def import_interfaces(request):
try:
reader = await request.multipart()
config_text = None
selected_interfaces = None
while True:
field = await reader.next()
if field is None:
break
if field.name == 'config':
config_text = ''
while True:
chunk = await field.read_chunk()
if not chunk:
break
config_text += chunk.decode('utf-8')
elif field.name == 'selected_interfaces':
data = await field.read(decode=True)
selected_interfaces = json.loads(data)
print(f"Selected interfaces: {selected_interfaces}")
current_interface = None
interface_config = {}
for line in config_text.splitlines():
line = line.strip()
if not line:
continue
if line.startswith("[[") and line.endswith("]]"):
if current_interface and current_interface["name"] in selected_interfaces:
name = current_interface["name"]
interface_config[name] = {
"type": current_interface.get("type"),
"interface_enabled": "true",
"target_host": current_interface.get("target_host"),
"target_port": current_interface.get("target_port"),
"listen_ip": current_interface.get("listen_ip"),
"listen_port": current_interface.get("listen_port"),
"forward_ip": current_interface.get("forward_ip"),
"forward_port": current_interface.get("forward_port"),
"port": current_interface.get("port"),
"frequency": current_interface.get("frequency"),
"bandwidth": current_interface.get("bandwidth"),
"txpower": current_interface.get("txpower"),
"spreadingfactor": current_interface.get("spreadingfactor"),
"codingrate": current_interface.get("codingrate")
}
interface_config[name] = {k: v for k, v in interface_config[name].items() if v is not None}
name = line[2:-2]
current_interface = {"name": name}
elif current_interface is not None and "=" in line:
key, value = [x.strip() for x in line.split("=", 1)]
value = value.strip('"').strip("'")
current_interface[key] = value
if current_interface and current_interface["name"] in selected_interfaces:
name = current_interface["name"]
interface_config[name] = {
"type": current_interface.get("type"),
"interface_enabled": "true",
"target_host": current_interface.get("target_host"),
"target_port": current_interface.get("target_port"),
"listen_ip": current_interface.get("listen_ip"),
"listen_port": current_interface.get("listen_port"),
"forward_ip": current_interface.get("forward_ip"),
"forward_port": current_interface.get("forward_port"),
"port": current_interface.get("port"),
"frequency": current_interface.get("frequency"),
"bandwidth": current_interface.get("bandwidth"),
"txpower": current_interface.get("txpower"),
"spreadingfactor": current_interface.get("spreadingfactor"),
"codingrate": current_interface.get("codingrate")
}
interface_config[name] = {k: v for k, v in interface_config[name].items() if v is not None}
# update reticulum config with new interfaces
self.reticulum.config["interfaces"].update(interface_config)
print("Final interfaces config:", self.reticulum.config["interfaces"])
self.reticulum.config.write()
return web.json_response({"message": "Interfaces imported successfully"})
except Exception as e:
print(f"Import error: {str(e)}")
print(f"Config text: {config_text}")
return web.json_response({
"message": f"Failed to import interfaces: {str(e)}"
}, status=500)
# handle websocket clients
@routes.get("/ws")

View file

@ -17,7 +17,8 @@
</button>
</div>
<div>
<div class="flex space-x-2">
<!-- Add Interface button -->
<RouterLink :to="{ name: 'interfaces.add' }">
<button type="button"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 dark:bg-zinc-700 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 dark:hover:bg-zinc-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:focus-visible:outline-zinc-700">
@ -27,6 +28,24 @@
<span>Add Interface</span>
</button>
</RouterLink>
<!-- Export button -->
<button @click="exportInterfaces" type="button"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 dark:bg-zinc-700 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 dark:hover:bg-zinc-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:focus-visible:outline-zinc-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
<span>Export</span>
</button>
<!-- Import button -->
<button @click="showImportDialog" type="button"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 dark:bg-zinc-700 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 dark:hover:bg-zinc-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:focus-visible:outline-zinc-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
<span>Import</span>
</button>
</div>
<!-- enabled interfaces -->
@ -49,6 +68,66 @@
@delete="deleteInterface(iface._name)"/>
</div>
</div>
<!-- Import Dialog -->
<div v-if="showingImportDialog" class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity flex items-center justify-center">
<div class="bg-white dark:bg-zinc-900 rounded-lg shadow-xl max-w-2xl w-full mx-4">
<div class="p-4 border-b dark:border-zinc-700">
<h3 class="text-lg font-semibold dark:text-white">Import Interfaces</h3>
</div>
<div class="p-4 space-y-4">
<!-- File Input -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-zinc-200">Select Configuration File</label>
<input type="file"
@change="onFileSelected"
accept=".conf"
class="mt-1 block w-full text-sm text-gray-500 dark:text-zinc-400
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-gray-500 file:text-white
hover:file:bg-gray-400
dark:file:bg-zinc-700 dark:hover:file:bg-zinc-600">
</div>
<!-- Interface Selection -->
<div v-if="importableInterfaces.length > 0">
<div class="flex justify-between mb-2">
<label class="block text-sm font-medium text-gray-700 dark:text-zinc-200">Select Interfaces to Import</label>
<div class="space-x-2">
<button @click="selectAllInterfaces" class="text-sm text-blue-500">Select All</button>
<button @click="deselectAllInterfaces" class="text-sm text-blue-500">Deselect All</button>
</div>
</div>
<div class="space-y-2 max-h-60 overflow-y-auto">
<div v-for="iface in importableInterfaces" :key="iface.name"
class="flex items-center p-2 border rounded dark:border-zinc-700">
<input type="checkbox"
v-model="selectedInterfaces"
:value="iface.name"
class="h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-zinc-600">
<label class="ml-2 text-sm text-gray-700 dark:text-zinc-200">
{{ iface.name }} ({{ iface.type }})
</label>
</div>
</div>
</div>
</div>
<div class="p-4 border-t dark:border-zinc-700 flex justify-end space-x-2">
<button @click="closeImportDialog"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 dark:bg-zinc-800 dark:text-zinc-200 dark:border-zinc-600 dark:hover:bg-zinc-700">
Cancel
</button>
<button @click="importSelectedInterfaces"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600">
Import Selected
</button>
</div>
</div>
</div>
</template>
<script>
@ -67,6 +146,10 @@ export default {
interfaces: {},
interfaceStats: {},
reloadInterval: null,
showingImportDialog: false,
importableInterfaces: [],
selectedInterfaces: [],
importFile: null
};
},
beforeUnmount() {
@ -219,6 +302,91 @@ export default {
await this.loadInterfaces();
},
async exportInterfaces() {
try {
const response = await window.axios.get('/api/v1/reticulum/interfaces/export', {
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'reticulum_interfaces');
document.body.appendChild(link);
link.click();
link.remove();
} catch(e) {
DialogUtils.alert("Failed to export interfaces");
console.error(e);
}
},
showImportDialog() {
this.showingImportDialog = true;
this.importableInterfaces = [];
this.selectedInterfaces = [];
this.importFile = null;
},
closeImportDialog() {
this.showingImportDialog = false;
},
async onFileSelected(event) {
const file = event.target.files[0];
if (!file) return;
this.importFile = file;
this.importableInterfaces = [];
this.selectedInterfaces = [];
const formData = new FormData();
formData.append('config', file);
try {
const response = await window.axios.post('/api/v1/reticulum/interfaces/preview', formData);
if (response.data.interfaces && response.data.interfaces.length > 0) {
this.importableInterfaces = response.data.interfaces;
this.selectedInterfaces = this.importableInterfaces.map(i => i.name);
} else {
DialogUtils.alert("No valid interfaces found in configuration file");
this.closeImportDialog();
}
} catch(e) {
DialogUtils.alert("Failed to parse configuration file");
console.error(e);
this.closeImportDialog();
}
},
selectAllInterfaces() {
this.selectedInterfaces = this.importableInterfaces.map(i => i.name);
},
deselectAllInterfaces() {
this.selectedInterfaces = [];
},
async importSelectedInterfaces() {
if (!this.importFile) {
DialogUtils.alert("Please select a configuration file");
return;
}
if (this.selectedInterfaces.length === 0) {
DialogUtils.alert("Please select at least one interface to import");
return;
}
const formData = new FormData();
formData.append('config', this.importFile);
formData.append('selected_interfaces', JSON.stringify(this.selectedInterfaces));
try {
await window.axios.post('/api/v1/reticulum/interfaces/import', formData);
await this.loadInterfaces();
this.closeImportDialog();
DialogUtils.alert("Interfaces imported successfully");
} catch(e) {
const message = e.response?.data?.message || "Failed to import interfaces";
DialogUtils.alert(message);
console.error(e);
}
}
},
computed: {
isElectron() {